integrateledger 0.1.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ledger
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
13
+ all 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.
@@ -0,0 +1,9 @@
1
+ include README.md
2
+ include LICENSE
3
+ include pyproject.toml
4
+ recursive-include ledger_sdk *.py
5
+ recursive-include integrateledger *.py
6
+ prune tests
7
+ prune build
8
+ prune dist
9
+ global-exclude *.pyc __pycache__ .DS_Store
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: integrateledger
3
+ Version: 0.1.0
4
+ Summary: Python client for the Ledger billing and usage API
5
+ License-Expression: MIT
6
+ Project-URL: Documentation, https://docs.integrateledger.com/python-sdk
7
+ Project-URL: Homepage, https://docs.integrateledger.com
8
+ Keywords: ledger,billing,usage,credits,sdk
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # integrateledger
22
+
23
+ The server-side Python SDK for Ledger's billing and usage API. Install it with
24
+ `python3 -m pip install integrateledger`, then use
25
+ `from integrateledger import ledger`. The capitalized `Ledger` class and
26
+ `ledger_sdk` import remain available for existing source users. See the
27
+ [Ledger docs](https://docs.integrateledger.com/python-sdk) for examples.
28
+
29
+ From a repository checkout, run the SDK tests with:
30
+
31
+ ```bash
32
+ python3 -m pip install ./packages/sdk-python
33
+ python3 -m unittest discover -s packages/sdk-python/tests -v
34
+ ```
35
+
36
+ ```python
37
+ import os
38
+ from integrateledger import ledger
39
+
40
+ client = ledger(api_key=os.environ["LEDGER_API_KEY"])
41
+ client.create_feature(id="api_call", name="API call", credit_cost=1)
42
+ client.create_plan(id="starter", name="Starter", monthly_credits=100, monthly_price_cents=1900)
43
+ client.create_add_on(id="extra_credits", name="Extra credits", credit_amount=50, price_cents=500)
44
+ client.create_or_get_customer(customer_id="buyer_123")
45
+ customer = client.get_customer(customer_id="buyer_123")
46
+ subscriptions = client.list_customer_subscriptions(customer_id="buyer_123")
47
+ purchases = client.list_customer_purchases(customer_id="buyer_123")
48
+ # Use an active paid subscription ID from the subscriptions list.
49
+ client.set_subscription_cancellation(customer_id="buyer_123", subscription_id=42, cancel_at_period_end=True)
50
+ ```
51
+
52
+ Methods return response dictionaries and use Python `snake_case` arguments. The client maps them to Ledger's JSON contract and sends the organization key in the `Authorization` header. Pass `base_url="http://127.0.0.1:3001"` for a local API.
53
+
54
+ Use a `ledger_test_` key for Sandbox or a `ledger_live_` key for Live. The key chooses the environment; the client methods and API URL stay the same. Store these keys in separate server-side variables, and complete sandbox validation before enabling live payments.
@@ -0,0 +1,34 @@
1
+ # integrateledger
2
+
3
+ The server-side Python SDK for Ledger's billing and usage API. Install it with
4
+ `python3 -m pip install integrateledger`, then use
5
+ `from integrateledger import ledger`. The capitalized `Ledger` class and
6
+ `ledger_sdk` import remain available for existing source users. See the
7
+ [Ledger docs](https://docs.integrateledger.com/python-sdk) for examples.
8
+
9
+ From a repository checkout, run the SDK tests with:
10
+
11
+ ```bash
12
+ python3 -m pip install ./packages/sdk-python
13
+ python3 -m unittest discover -s packages/sdk-python/tests -v
14
+ ```
15
+
16
+ ```python
17
+ import os
18
+ from integrateledger import ledger
19
+
20
+ client = ledger(api_key=os.environ["LEDGER_API_KEY"])
21
+ client.create_feature(id="api_call", name="API call", credit_cost=1)
22
+ client.create_plan(id="starter", name="Starter", monthly_credits=100, monthly_price_cents=1900)
23
+ client.create_add_on(id="extra_credits", name="Extra credits", credit_amount=50, price_cents=500)
24
+ client.create_or_get_customer(customer_id="buyer_123")
25
+ customer = client.get_customer(customer_id="buyer_123")
26
+ subscriptions = client.list_customer_subscriptions(customer_id="buyer_123")
27
+ purchases = client.list_customer_purchases(customer_id="buyer_123")
28
+ # Use an active paid subscription ID from the subscriptions list.
29
+ client.set_subscription_cancellation(customer_id="buyer_123", subscription_id=42, cancel_at_period_end=True)
30
+ ```
31
+
32
+ Methods return response dictionaries and use Python `snake_case` arguments. The client maps them to Ledger's JSON contract and sends the organization key in the `Authorization` header. Pass `base_url="http://127.0.0.1:3001"` for a local API.
33
+
34
+ Use a `ledger_test_` key for Sandbox or a `ledger_live_` key for Live. The key chooses the environment; the client methods and API URL stay the same. Store these keys in separate server-side variables, and complete sandbox validation before enabling live payments.
@@ -0,0 +1,9 @@
1
+ """Public Python import for the Ledger client."""
2
+
3
+ from ledger_sdk import Ledger, LedgerAPIError, LedgerConnectionError
4
+
5
+ # Keep the requested short constructor spelling while retaining the existing
6
+ # capitalized class and ledger_sdk imports for existing source consumers.
7
+ ledger = Ledger
8
+
9
+ __all__ = ["ledger", "Ledger", "LedgerAPIError", "LedgerConnectionError"]
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: integrateledger
3
+ Version: 0.1.0
4
+ Summary: Python client for the Ledger billing and usage API
5
+ License-Expression: MIT
6
+ Project-URL: Documentation, https://docs.integrateledger.com/python-sdk
7
+ Project-URL: Homepage, https://docs.integrateledger.com
8
+ Keywords: ledger,billing,usage,credits,sdk
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # integrateledger
22
+
23
+ The server-side Python SDK for Ledger's billing and usage API. Install it with
24
+ `python3 -m pip install integrateledger`, then use
25
+ `from integrateledger import ledger`. The capitalized `Ledger` class and
26
+ `ledger_sdk` import remain available for existing source users. See the
27
+ [Ledger docs](https://docs.integrateledger.com/python-sdk) for examples.
28
+
29
+ From a repository checkout, run the SDK tests with:
30
+
31
+ ```bash
32
+ python3 -m pip install ./packages/sdk-python
33
+ python3 -m unittest discover -s packages/sdk-python/tests -v
34
+ ```
35
+
36
+ ```python
37
+ import os
38
+ from integrateledger import ledger
39
+
40
+ client = ledger(api_key=os.environ["LEDGER_API_KEY"])
41
+ client.create_feature(id="api_call", name="API call", credit_cost=1)
42
+ client.create_plan(id="starter", name="Starter", monthly_credits=100, monthly_price_cents=1900)
43
+ client.create_add_on(id="extra_credits", name="Extra credits", credit_amount=50, price_cents=500)
44
+ client.create_or_get_customer(customer_id="buyer_123")
45
+ customer = client.get_customer(customer_id="buyer_123")
46
+ subscriptions = client.list_customer_subscriptions(customer_id="buyer_123")
47
+ purchases = client.list_customer_purchases(customer_id="buyer_123")
48
+ # Use an active paid subscription ID from the subscriptions list.
49
+ client.set_subscription_cancellation(customer_id="buyer_123", subscription_id=42, cancel_at_period_end=True)
50
+ ```
51
+
52
+ Methods return response dictionaries and use Python `snake_case` arguments. The client maps them to Ledger's JSON contract and sends the organization key in the `Authorization` header. Pass `base_url="http://127.0.0.1:3001"` for a local API.
53
+
54
+ Use a `ledger_test_` key for Sandbox or a `ledger_live_` key for Live. The key chooses the environment; the client methods and API URL stay the same. Store these keys in separate server-side variables, and complete sandbox validation before enabling live payments.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ integrateledger/__init__.py
7
+ integrateledger.egg-info/PKG-INFO
8
+ integrateledger.egg-info/SOURCES.txt
9
+ integrateledger.egg-info/dependency_links.txt
10
+ integrateledger.egg-info/top_level.txt
11
+ ledger_sdk/__init__.py
12
+ ledger_sdk/client.py
@@ -0,0 +1,2 @@
1
+ integrateledger
2
+ ledger_sdk
@@ -0,0 +1,5 @@
1
+ """Python client for Ledger's sandbox API."""
2
+
3
+ from .client import Ledger, LedgerAPIError, LedgerConnectionError
4
+
5
+ __all__ = ["Ledger", "LedgerAPIError", "LedgerConnectionError"]
@@ -0,0 +1,216 @@
1
+ """Small, dependency-free client for Ledger's public HTTP API.
2
+
3
+ All method arguments use Python snake_case; JSON sent to Ledger uses the API's
4
+ documented camelCase fields (and request_id for usage tracking).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any, Dict, Mapping, Optional
11
+ from urllib.error import HTTPError, URLError
12
+ from urllib.parse import quote
13
+ from urllib.request import Request, urlopen
14
+
15
+
16
+ class LedgerAPIError(Exception):
17
+ """An HTTP error returned by Ledger."""
18
+
19
+ def __init__(self, status: int, code: str, body: Any):
20
+ self.status = status
21
+ self.code = code
22
+ self.body = body
23
+ super().__init__(f"Ledger API returned {status}: {code}")
24
+
25
+
26
+ class LedgerConnectionError(Exception):
27
+ """Ledger could not be reached before a response was received."""
28
+
29
+
30
+ class Ledger:
31
+ """Organization-scoped client. Keep ``api_key`` on your server."""
32
+
33
+ def __init__(
34
+ self,
35
+ *,
36
+ api_key: str,
37
+ base_url: str = "https://api.integrateledger.com",
38
+ timeout: float = 30.0,
39
+ ):
40
+ if not api_key:
41
+ raise ValueError("api_key is required")
42
+ if not base_url.startswith(("https://", "http://")):
43
+ raise ValueError("base_url must be an HTTP URL")
44
+ if timeout <= 0:
45
+ raise ValueError("timeout must be positive")
46
+ self.api_key = api_key
47
+ self.base_url = base_url.rstrip("/")
48
+ self.timeout = timeout
49
+
50
+ def _request(
51
+ self,
52
+ method: str,
53
+ path: str,
54
+ body: Optional[Mapping[str, Any]] = None,
55
+ *,
56
+ idempotency_key: Optional[str] = None,
57
+ ) -> Dict[str, Any]:
58
+ headers = {
59
+ "Accept": "application/json",
60
+ "Authorization": f"Bearer {self.api_key}",
61
+ }
62
+ data = None
63
+ if body is not None:
64
+ headers["Content-Type"] = "application/json"
65
+ data = json.dumps(body, separators=(",", ":")).encode("utf-8")
66
+ if idempotency_key is not None:
67
+ headers["Idempotency-Key"] = idempotency_key
68
+ request = Request(self.base_url + path, data=data, headers=headers, method=method)
69
+ try:
70
+ with urlopen(request, timeout=self.timeout) as response:
71
+ payload = response.read()
72
+ return json.loads(payload) if payload else {}
73
+ except HTTPError as error:
74
+ payload = error.read()
75
+ try:
76
+ details = json.loads(payload) if payload else {}
77
+ except (ValueError, TypeError):
78
+ details = {"error": payload.decode("utf-8", "replace")}
79
+ code = details.get("error", "http_error") if isinstance(details, dict) else "http_error"
80
+ raise LedgerAPIError(error.code, code, details) from error
81
+ except URLError as error:
82
+ raise LedgerConnectionError(str(error.reason)) from error
83
+
84
+ @staticmethod
85
+ def _item_path(kind: str, item_id: str) -> str:
86
+ return f"/v1/{kind}/{quote(item_id, safe='')}"
87
+
88
+ # Catalog
89
+ def publish_catalog(self, catalog: Mapping[str, Any]) -> Dict[str, Any]:
90
+ return self._request("PUT", "/v1/catalog", catalog)
91
+
92
+ def list_plans(self) -> Dict[str, Any]:
93
+ return self._request("GET", "/v1/plans")
94
+
95
+ def create_plan(self, *, id: str, name: str, monthly_credits: int, monthly_price_cents: int) -> Dict[str, Any]:
96
+ return self._request("POST", "/v1/plans", {
97
+ "id": id, "name": name, "monthlyCredits": monthly_credits,
98
+ "monthlyPriceCents": monthly_price_cents,
99
+ })
100
+
101
+ def get_plan(self, id: str) -> Dict[str, Any]:
102
+ return self._request("GET", self._item_path("plans", id))
103
+
104
+ def update_plan(self, id: str, *, name: str, monthly_credits: int, monthly_price_cents: int) -> Dict[str, Any]:
105
+ return self._request("PUT", self._item_path("plans", id), {
106
+ "name": name, "monthlyCredits": monthly_credits,
107
+ "monthlyPriceCents": monthly_price_cents,
108
+ })
109
+
110
+ def archive_plan(self, id: str) -> Dict[str, Any]:
111
+ return self._request("DELETE", self._item_path("plans", id))
112
+
113
+ def list_features(self) -> Dict[str, Any]:
114
+ return self._request("GET", "/v1/features")
115
+
116
+ def create_feature(self, *, id: str, name: str, credit_cost: int) -> Dict[str, Any]:
117
+ return self._request("POST", "/v1/features", {"id": id, "name": name, "creditCost": credit_cost})
118
+
119
+ def get_feature(self, id: str) -> Dict[str, Any]:
120
+ return self._request("GET", self._item_path("features", id))
121
+
122
+ def update_feature(self, id: str, *, name: str, credit_cost: int) -> Dict[str, Any]:
123
+ return self._request("PUT", self._item_path("features", id), {"name": name, "creditCost": credit_cost})
124
+
125
+ def archive_feature(self, id: str) -> Dict[str, Any]:
126
+ return self._request("DELETE", self._item_path("features", id))
127
+
128
+ def list_add_ons(self) -> Dict[str, Any]:
129
+ return self._request("GET", "/v1/add-ons")
130
+
131
+ def create_add_on(self, *, id: str, name: str, credit_amount: int, price_cents: int) -> Dict[str, Any]:
132
+ return self._request("POST", "/v1/add-ons", {
133
+ "id": id, "name": name, "type": "credit_pack",
134
+ "creditAmount": credit_amount, "priceCents": price_cents,
135
+ })
136
+
137
+ def get_add_on(self, id: str) -> Dict[str, Any]:
138
+ return self._request("GET", self._item_path("add-ons", id))
139
+
140
+ def update_add_on(self, id: str, *, name: str, credit_amount: int, price_cents: int) -> Dict[str, Any]:
141
+ return self._request("PUT", self._item_path("add-ons", id), {
142
+ "name": name, "type": "credit_pack", "creditAmount": credit_amount,
143
+ "priceCents": price_cents,
144
+ })
145
+
146
+ def archive_add_on(self, id: str) -> Dict[str, Any]:
147
+ return self._request("DELETE", self._item_path("add-ons", id))
148
+
149
+ # Customers, usage, and billing
150
+ def create_or_get_customer(self, *, customer_id: str) -> Dict[str, Any]:
151
+ return self._request("POST", "/v1/customers", {"customerId": customer_id})
152
+
153
+ def get_customer(self, *, customer_id: str) -> Dict[str, Any]:
154
+ return self._request("GET", f"/v1/customers?customerId={quote(customer_id, safe='')}")
155
+
156
+ def list_customer_subscriptions(
157
+ self, *, customer_id: str, limit: Optional[int] = None, cursor: Optional[int] = None
158
+ ) -> Dict[str, Any]:
159
+ return self._request("GET", self._customer_history_path("subscriptions", customer_id, limit, cursor))
160
+
161
+ def list_customer_purchases(
162
+ self, *, customer_id: str, limit: Optional[int] = None, cursor: Optional[int] = None
163
+ ) -> Dict[str, Any]:
164
+ return self._request("GET", self._customer_history_path("purchases", customer_id, limit, cursor))
165
+
166
+ @staticmethod
167
+ def _customer_history_path(kind: str, customer_id: str, limit: Optional[int], cursor: Optional[int]) -> str:
168
+ if limit is not None and (not isinstance(limit, int) or isinstance(limit, bool) or not 1 <= limit <= 100):
169
+ raise ValueError("limit must be an integer from 1 to 100")
170
+ if cursor is not None and (not isinstance(cursor, int) or isinstance(cursor, bool) or cursor < 1):
171
+ raise ValueError("cursor must be a positive integer")
172
+ path = f"/v1/customers/{kind}?customerId={quote(customer_id, safe='')}"
173
+ if limit is not None:
174
+ path += f"&limit={limit}"
175
+ if cursor is not None:
176
+ path += f"&cursor={cursor}"
177
+ return path
178
+
179
+ def set_subscription_cancellation(
180
+ self, *, customer_id: str, subscription_id: int, cancel_at_period_end: bool
181
+ ) -> Dict[str, Any]:
182
+ if not isinstance(subscription_id, int) or isinstance(subscription_id, bool) or subscription_id < 1:
183
+ raise ValueError("subscription_id must be a positive integer")
184
+ if not isinstance(cancel_at_period_end, bool):
185
+ raise ValueError("cancel_at_period_end must be a boolean")
186
+ return self._request(
187
+ "PUT",
188
+ f"/v1/customers/subscriptions/{subscription_id}/cancellation"
189
+ f"?customerId={quote(customer_id, safe='')}",
190
+ {"cancelAtPeriodEnd": cancel_at_period_end},
191
+ )
192
+
193
+ def check_credits(self, *, customer_id: str, feature_id: str) -> Dict[str, Any]:
194
+ return self._request("POST", "/v1/check", {"customerId": customer_id, "featureId": feature_id})
195
+
196
+ def track_usage(self, *, customer_id: str, feature_id: str, request_id: str) -> Dict[str, Any]:
197
+ return self._request("POST", "/v1/track", {
198
+ "customerId": customer_id, "featureId": feature_id, "request_id": request_id,
199
+ })
200
+
201
+ def attach(self, *, customer_id: str, plan_id: Optional[str] = None, add_on_id: Optional[str] = None) -> Dict[str, Any]:
202
+ if (plan_id is None) == (add_on_id is None):
203
+ raise ValueError("provide exactly one of plan_id or add_on_id")
204
+ body = {"customerId": customer_id}
205
+ if plan_id is not None:
206
+ body["planId"] = plan_id
207
+ else:
208
+ body["addOnId"] = add_on_id
209
+ return self._request("POST", "/v1/billing/attach", body)
210
+
211
+ def upgrade_paid_plan(self, *, customer_id: str, plan_id: str, idempotency_key: str) -> Dict[str, Any]:
212
+ if not idempotency_key:
213
+ raise ValueError("idempotency_key is required")
214
+ return self._request("POST", "/v1/billing/upgrade", {
215
+ "customerId": customer_id, "planId": plan_id,
216
+ }, idempotency_key=idempotency_key)
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "integrateledger"
7
+ version = "0.1.0"
8
+ description = "Python client for the Ledger billing and usage API"
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.9"
12
+ readme = "README.md"
13
+ keywords = ["ledger", "billing", "usage", "credits", "sdk"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.9",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Operating System :: OS Independent",
22
+ ]
23
+
24
+ [project.urls]
25
+ Documentation = "https://docs.integrateledger.com/python-sdk"
26
+ Homepage = "https://docs.integrateledger.com"
27
+
28
+ [tool.setuptools]
29
+ packages = ["ledger_sdk", "integrateledger"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Setuptools entry point; package metadata lives in pyproject.toml."""
2
+
3
+ from setuptools import setup
4
+
5
+ setup()