benvo 1.0.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.
benvo-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Coviola Group LLC
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.
benvo-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,257 @@
1
+ Metadata-Version: 2.1
2
+ Name: benvo
3
+ Version: 1.0.0
4
+ Summary: Official Python library for the Benvo API
5
+ Author: Coviola Group LLC
6
+ Author-email: Coviola Group LLC <support@benvo.io>
7
+ License: MIT
8
+ Project-URL: Homepage, https://developers.benvo.io
9
+ Project-URL: Documentation, https://developers.benvo.io
10
+ Keywords: benvo,api,sdk,hospitality,restaurants,members clubs,reservations,webhooks
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
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: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Benvo Python library
29
+
30
+ The official Python library for the [Benvo API](https://developers.benvo.io). Typed resources, automatic pagination, idempotent writes, retries with backoff, webhook verification and request signing, using only the Python standard library.
31
+
32
+ - Python 3.9 or later, no dependencies
33
+ - Type hints included (`py.typed`)
34
+ - Full reference: [developers.benvo.io](https://developers.benvo.io)
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install benvo
40
+ ```
41
+
42
+ ## Quickstart
43
+
44
+ ```python
45
+ import os
46
+ from benvo import Benvo
47
+
48
+ client = Benvo(os.environ["BENVO_API_KEY"])
49
+
50
+ ping = client.ping()
51
+ print(ping["account"]["name"], ping["key"]["scopes"])
52
+
53
+ person = client.people.create(first_name="Sophie", last_name="Tan", email="sophie.tan@example.com")
54
+ print(person["id"], person.last_response["status"])
55
+ ```
56
+
57
+ Objects come back as dictionaries (typed with `TypedDict`), so `person["id"]` works and they serialise with `json.dumps`. Each one also has `last_response` with `status`, `request_id`, `idempotent_replayed` and `headers`; `benvo.response_of(obj)` reads it with a type hint.
58
+
59
+ Keep the key on your server. The API refuses requests from browsers.
60
+
61
+ ### Options
62
+
63
+ ```python
64
+ client = Benvo(
65
+ api_key,
66
+ base_url="https://api.benvo.io",
67
+ timeout=30,
68
+ max_retries=2,
69
+ signing_secret=os.environ.get("BENVO_SIGNING_SECRET"),
70
+ )
71
+ ```
72
+
73
+ Every method also takes `request_options={"timeout": 5, "max_retries": 0, "headers": {...}}`, and writes take `idempotency_key`.
74
+
75
+ ## Resources
76
+
77
+ | Resource | Methods |
78
+ |---|---|
79
+ | `client.ping()` | Check the key, its account and scopes |
80
+ | `client.people` | `list`, `retrieve`, `create`, `update` |
81
+ | `client.members` | `list`, `retrieve` |
82
+ | `client.membership_tiers` | `list` |
83
+ | `client.leads` | `list`, `create` |
84
+ | `client.reservations` | `list`, `retrieve`, `create`, `update`, `cancel` |
85
+ | `client.availability` | `check` |
86
+ | `client.events` | `list`, `retrieve` |
87
+ | `client.rsvps` | `list(event_id)`, `create(event_id, ...)`, `update(event_id, rsvp_id, ...)` |
88
+ | `client.orders` | `list`, `retrieve` |
89
+ | `client.invoices` | `list`, `retrieve` |
90
+ | `client.payments` | `list`, `retrieve` |
91
+ | `client.menus` | `list`, `retrieve` |
92
+ | `client.products` | `list` |
93
+ | `client.meta` | `ip_ranges` (no key needed) |
94
+
95
+ Writes take the body as keyword arguments, as a dictionary, or both:
96
+
97
+ ```python
98
+ availability = client.availability.check(date="2026-11-12", party_size=4)
99
+ slot = next((s for s in availability["slots"] if s["available"]), None)
100
+
101
+ if slot:
102
+ reservation = client.reservations.create(
103
+ date=slot["date"],
104
+ time=slot["time"],
105
+ party_size=4,
106
+ guest={"name": "Sophie Tan", "email": "sophie.tan@example.com"},
107
+ )
108
+ client.reservations.update(reservation["id"], party_size=5)
109
+
110
+ client.rsvps.create("evt_8Hk2QmZ4", {"member_id": "mem_3fJ9xL2p", "guests": [{"name": "Rachel Lim"}]})
111
+ ```
112
+
113
+ On `update`, a value of `None` is sent as `null` and clears the field.
114
+
115
+ Ids are opaque strings with a prefix per object (`per_`, `mem_`, `res_` and so on). Store them as strings and never parse them. Money is always `{"amount": 24860, "currency": "SGD"}` with `amount` in minor units. Every object carries `livemode`.
116
+
117
+ Contact details and sensitive fields are only present when the key has the matching scope (`people:read_contact`, `people:read_sensitive`, `orders:read_spend`), so read them with `.get()`.
118
+
119
+ ## Pagination
120
+
121
+ `list()` returns an iterator that fetches the next page with `starting_after` until `has_more` is false.
122
+
123
+ ```python
124
+ for reservation in client.reservations.list(date_from="2026-11-01", date_to="2026-11-30", limit=100):
125
+ print(reservation["id"], reservation["status"])
126
+ ```
127
+
128
+ One page at a time:
129
+
130
+ ```python
131
+ page = client.people.list(limit=25).page()
132
+ print(len(page["data"]), page["has_more"], page["next_cursor"])
133
+
134
+ following = client.people.list(limit=25, starting_after=page["next_cursor"]).page()
135
+ ```
136
+
137
+ Also available: `.pages()` and `.to_list(max_items)`. Timestamp filters such as `updated_since` accept a `datetime` (time zone aware values are sent in UTC) or an ISO 8601 string; date filters accept a `date`.
138
+
139
+ ## Idempotency
140
+
141
+ Every create, update, cancel and RSVP write sends an `Idempotency-Key`. When you do not pass one, the library generates a uuid v4 and keeps the same key across its own retries, so a retried write never runs twice.
142
+
143
+ Pass your own key to make a write safe across restarts of your process:
144
+
145
+ ```python
146
+ client.leads.create(
147
+ email="marcus.wong@example.com",
148
+ message="Dinner for 30 in December",
149
+ idempotency_key=f"enquiry-{form_submission_id}",
150
+ )
151
+ ```
152
+
153
+ A replayed result has `last_response["idempotent_replayed"] == True`. Reusing a key with a different body fails with `idempotency_key_reused`.
154
+
155
+ ## Errors
156
+
157
+ Every API error is a `BenvoError` subclass with `status`, `type`, `code`, `param`, `request_id`, `headers` and `raw`.
158
+
159
+ | Exception | When |
160
+ |---|---|
161
+ | `BenvoAuthenticationError` | 401: missing, invalid, expired or revoked key |
162
+ | `BenvoPermissionError` | 403: missing scope, module not enabled, account inactive, consent required |
163
+ | `BenvoNotFoundError` | 404: no such record in this account |
164
+ | `BenvoInvalidRequestError` | 400, 409, 413, 422: invalid parameters or a conflict such as `slot_unavailable` |
165
+ | `BenvoRateLimitError` | 429 after the retries; `retry_after` holds the seconds to wait |
166
+ | `BenvoApiError` | 5xx after the retries, or a response that is not JSON |
167
+ | `BenvoConnectionError` | The API could not be reached; `timed_out` is true for timeouts |
168
+
169
+ ```python
170
+ from benvo import BenvoInvalidRequestError
171
+
172
+ try:
173
+ client.reservations.create(date="2026-11-12", time="19:00", party_size=4, guest={"name": "Sophie Tan", "email": "sophie.tan@example.com"})
174
+ except BenvoInvalidRequestError as error:
175
+ if error.code != "slot_unavailable":
176
+ raise
177
+ print("That time was just taken.")
178
+ ```
179
+
180
+ Quote `request_id` when you contact Benvo support.
181
+
182
+ ## Retries
183
+
184
+ The library retries up to `max_retries` times (default 2) on:
185
+
186
+ - 429 rate limits
187
+ - 409 `idempotency_in_progress`
188
+ - 5xx responses
189
+ - network errors and timeouts
190
+
191
+ It waits for `Retry-After` when the API sends it (up to 60 seconds) and otherwise backs off exponentially from 0.5 seconds up to 8 seconds with jitter. Set `max_retries=0` to turn retries off.
192
+
193
+ ## Webhooks
194
+
195
+ Benvo signs every webhook with [Standard Webhooks](https://www.standardwebhooks.com) headers. Verify the raw body, before any JSON parsing:
196
+
197
+ ```python
198
+ import os
199
+ from flask import Flask, request
200
+ from benvo import BenvoWebhookVerificationError, webhooks
201
+
202
+ app = Flask(__name__)
203
+
204
+ @app.post("/benvo/webhooks")
205
+ def benvo_webhook():
206
+ try:
207
+ event = webhooks.verify(request.get_data(), request.headers, os.environ["BENVO_WEBHOOK_SECRET"])
208
+ except BenvoWebhookVerificationError:
209
+ return "", 400
210
+
211
+ if event["type"] == "reservation.created":
212
+ print("New booking", event["data"]["object"]["id"])
213
+ return "", 204
214
+ ```
215
+
216
+ `webhooks.verify(payload, headers, secret, tolerance=300)` checks the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers, refuses timestamps more than `tolerance` seconds away from now, compares signatures in constant time and returns the parsed event. `headers` can be any mapping (header names in any case) or a list of pairs. Store `event["id"]` and skip ids you have already processed.
217
+
218
+ Endpoints set to thin payloads receive only `id` and `object` in `data.object`; fetch the full object with your key.
219
+
220
+ For your own tests, `webhooks.sign(body, secret, msg_id, timestamp=None)` returns signed headers.
221
+
222
+ ## Request signing
223
+
224
+ Keys marked to require signatures must sign every request. Pass the key's signing secret and the library adds the header:
225
+
226
+ ```python
227
+ client = Benvo(api_key, signing_secret=os.environ["BENVO_SIGNING_SECRET"])
228
+ ```
229
+
230
+ ```
231
+ Benvo-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.METHOD.path.body">
232
+ ```
233
+
234
+ `path` is the path with its query string exactly as sent (for example `/v1/people?limit=25`) and `body` is the exact JSON sent, or empty for GET. A new signature is made for each retry. Requests older than 5 minutes are refused, so keep your server clock in sync. To sign by hand, use `benvo.sign_request(secret, method, path, body, timestamp=None)`.
235
+
236
+ ## Test keys and live keys
237
+
238
+ | Key | Works on | `livemode` |
239
+ |---|---|---|
240
+ | `bvk_test_...` | Your sandbox account only, with fake data. Nothing reaches real guests | `False` |
241
+ | `bvk_live_...` | The venue account it was issued for, after the venue owner approves it | `True` |
242
+
243
+ The same code runs against both. `client.livemode` tells you which kind of key the client holds, and every object and webhook event carries `livemode`. Build and test with a test key, then swap in the live key.
244
+
245
+ ## Development
246
+
247
+ Run from `packages/sdk-python`:
248
+
249
+ ```bash
250
+ python3 -m unittest discover -s tests # offline test suite
251
+ python3 scripts/build_dist.py # dist/benvo-1.0.0.tar.gz and dist/benvo-1.0.0-py3-none-any.whl, standard library only
252
+ python3 -m build # the same archives with the official builder, when the build package is installed
253
+ ```
254
+
255
+ ## Licence
256
+
257
+ MIT. Copyright Coviola Group LLC.
benvo-1.0.0/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # Benvo Python library
2
+
3
+ The official Python library for the [Benvo API](https://developers.benvo.io). Typed resources, automatic pagination, idempotent writes, retries with backoff, webhook verification and request signing, using only the Python standard library.
4
+
5
+ - Python 3.9 or later, no dependencies
6
+ - Type hints included (`py.typed`)
7
+ - Full reference: [developers.benvo.io](https://developers.benvo.io)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install benvo
13
+ ```
14
+
15
+ ## Quickstart
16
+
17
+ ```python
18
+ import os
19
+ from benvo import Benvo
20
+
21
+ client = Benvo(os.environ["BENVO_API_KEY"])
22
+
23
+ ping = client.ping()
24
+ print(ping["account"]["name"], ping["key"]["scopes"])
25
+
26
+ person = client.people.create(first_name="Sophie", last_name="Tan", email="sophie.tan@example.com")
27
+ print(person["id"], person.last_response["status"])
28
+ ```
29
+
30
+ Objects come back as dictionaries (typed with `TypedDict`), so `person["id"]` works and they serialise with `json.dumps`. Each one also has `last_response` with `status`, `request_id`, `idempotent_replayed` and `headers`; `benvo.response_of(obj)` reads it with a type hint.
31
+
32
+ Keep the key on your server. The API refuses requests from browsers.
33
+
34
+ ### Options
35
+
36
+ ```python
37
+ client = Benvo(
38
+ api_key,
39
+ base_url="https://api.benvo.io",
40
+ timeout=30,
41
+ max_retries=2,
42
+ signing_secret=os.environ.get("BENVO_SIGNING_SECRET"),
43
+ )
44
+ ```
45
+
46
+ Every method also takes `request_options={"timeout": 5, "max_retries": 0, "headers": {...}}`, and writes take `idempotency_key`.
47
+
48
+ ## Resources
49
+
50
+ | Resource | Methods |
51
+ |---|---|
52
+ | `client.ping()` | Check the key, its account and scopes |
53
+ | `client.people` | `list`, `retrieve`, `create`, `update` |
54
+ | `client.members` | `list`, `retrieve` |
55
+ | `client.membership_tiers` | `list` |
56
+ | `client.leads` | `list`, `create` |
57
+ | `client.reservations` | `list`, `retrieve`, `create`, `update`, `cancel` |
58
+ | `client.availability` | `check` |
59
+ | `client.events` | `list`, `retrieve` |
60
+ | `client.rsvps` | `list(event_id)`, `create(event_id, ...)`, `update(event_id, rsvp_id, ...)` |
61
+ | `client.orders` | `list`, `retrieve` |
62
+ | `client.invoices` | `list`, `retrieve` |
63
+ | `client.payments` | `list`, `retrieve` |
64
+ | `client.menus` | `list`, `retrieve` |
65
+ | `client.products` | `list` |
66
+ | `client.meta` | `ip_ranges` (no key needed) |
67
+
68
+ Writes take the body as keyword arguments, as a dictionary, or both:
69
+
70
+ ```python
71
+ availability = client.availability.check(date="2026-11-12", party_size=4)
72
+ slot = next((s for s in availability["slots"] if s["available"]), None)
73
+
74
+ if slot:
75
+ reservation = client.reservations.create(
76
+ date=slot["date"],
77
+ time=slot["time"],
78
+ party_size=4,
79
+ guest={"name": "Sophie Tan", "email": "sophie.tan@example.com"},
80
+ )
81
+ client.reservations.update(reservation["id"], party_size=5)
82
+
83
+ client.rsvps.create("evt_8Hk2QmZ4", {"member_id": "mem_3fJ9xL2p", "guests": [{"name": "Rachel Lim"}]})
84
+ ```
85
+
86
+ On `update`, a value of `None` is sent as `null` and clears the field.
87
+
88
+ Ids are opaque strings with a prefix per object (`per_`, `mem_`, `res_` and so on). Store them as strings and never parse them. Money is always `{"amount": 24860, "currency": "SGD"}` with `amount` in minor units. Every object carries `livemode`.
89
+
90
+ Contact details and sensitive fields are only present when the key has the matching scope (`people:read_contact`, `people:read_sensitive`, `orders:read_spend`), so read them with `.get()`.
91
+
92
+ ## Pagination
93
+
94
+ `list()` returns an iterator that fetches the next page with `starting_after` until `has_more` is false.
95
+
96
+ ```python
97
+ for reservation in client.reservations.list(date_from="2026-11-01", date_to="2026-11-30", limit=100):
98
+ print(reservation["id"], reservation["status"])
99
+ ```
100
+
101
+ One page at a time:
102
+
103
+ ```python
104
+ page = client.people.list(limit=25).page()
105
+ print(len(page["data"]), page["has_more"], page["next_cursor"])
106
+
107
+ following = client.people.list(limit=25, starting_after=page["next_cursor"]).page()
108
+ ```
109
+
110
+ Also available: `.pages()` and `.to_list(max_items)`. Timestamp filters such as `updated_since` accept a `datetime` (time zone aware values are sent in UTC) or an ISO 8601 string; date filters accept a `date`.
111
+
112
+ ## Idempotency
113
+
114
+ Every create, update, cancel and RSVP write sends an `Idempotency-Key`. When you do not pass one, the library generates a uuid v4 and keeps the same key across its own retries, so a retried write never runs twice.
115
+
116
+ Pass your own key to make a write safe across restarts of your process:
117
+
118
+ ```python
119
+ client.leads.create(
120
+ email="marcus.wong@example.com",
121
+ message="Dinner for 30 in December",
122
+ idempotency_key=f"enquiry-{form_submission_id}",
123
+ )
124
+ ```
125
+
126
+ A replayed result has `last_response["idempotent_replayed"] == True`. Reusing a key with a different body fails with `idempotency_key_reused`.
127
+
128
+ ## Errors
129
+
130
+ Every API error is a `BenvoError` subclass with `status`, `type`, `code`, `param`, `request_id`, `headers` and `raw`.
131
+
132
+ | Exception | When |
133
+ |---|---|
134
+ | `BenvoAuthenticationError` | 401: missing, invalid, expired or revoked key |
135
+ | `BenvoPermissionError` | 403: missing scope, module not enabled, account inactive, consent required |
136
+ | `BenvoNotFoundError` | 404: no such record in this account |
137
+ | `BenvoInvalidRequestError` | 400, 409, 413, 422: invalid parameters or a conflict such as `slot_unavailable` |
138
+ | `BenvoRateLimitError` | 429 after the retries; `retry_after` holds the seconds to wait |
139
+ | `BenvoApiError` | 5xx after the retries, or a response that is not JSON |
140
+ | `BenvoConnectionError` | The API could not be reached; `timed_out` is true for timeouts |
141
+
142
+ ```python
143
+ from benvo import BenvoInvalidRequestError
144
+
145
+ try:
146
+ client.reservations.create(date="2026-11-12", time="19:00", party_size=4, guest={"name": "Sophie Tan", "email": "sophie.tan@example.com"})
147
+ except BenvoInvalidRequestError as error:
148
+ if error.code != "slot_unavailable":
149
+ raise
150
+ print("That time was just taken.")
151
+ ```
152
+
153
+ Quote `request_id` when you contact Benvo support.
154
+
155
+ ## Retries
156
+
157
+ The library retries up to `max_retries` times (default 2) on:
158
+
159
+ - 429 rate limits
160
+ - 409 `idempotency_in_progress`
161
+ - 5xx responses
162
+ - network errors and timeouts
163
+
164
+ It waits for `Retry-After` when the API sends it (up to 60 seconds) and otherwise backs off exponentially from 0.5 seconds up to 8 seconds with jitter. Set `max_retries=0` to turn retries off.
165
+
166
+ ## Webhooks
167
+
168
+ Benvo signs every webhook with [Standard Webhooks](https://www.standardwebhooks.com) headers. Verify the raw body, before any JSON parsing:
169
+
170
+ ```python
171
+ import os
172
+ from flask import Flask, request
173
+ from benvo import BenvoWebhookVerificationError, webhooks
174
+
175
+ app = Flask(__name__)
176
+
177
+ @app.post("/benvo/webhooks")
178
+ def benvo_webhook():
179
+ try:
180
+ event = webhooks.verify(request.get_data(), request.headers, os.environ["BENVO_WEBHOOK_SECRET"])
181
+ except BenvoWebhookVerificationError:
182
+ return "", 400
183
+
184
+ if event["type"] == "reservation.created":
185
+ print("New booking", event["data"]["object"]["id"])
186
+ return "", 204
187
+ ```
188
+
189
+ `webhooks.verify(payload, headers, secret, tolerance=300)` checks the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers, refuses timestamps more than `tolerance` seconds away from now, compares signatures in constant time and returns the parsed event. `headers` can be any mapping (header names in any case) or a list of pairs. Store `event["id"]` and skip ids you have already processed.
190
+
191
+ Endpoints set to thin payloads receive only `id` and `object` in `data.object`; fetch the full object with your key.
192
+
193
+ For your own tests, `webhooks.sign(body, secret, msg_id, timestamp=None)` returns signed headers.
194
+
195
+ ## Request signing
196
+
197
+ Keys marked to require signatures must sign every request. Pass the key's signing secret and the library adds the header:
198
+
199
+ ```python
200
+ client = Benvo(api_key, signing_secret=os.environ["BENVO_SIGNING_SECRET"])
201
+ ```
202
+
203
+ ```
204
+ Benvo-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.METHOD.path.body">
205
+ ```
206
+
207
+ `path` is the path with its query string exactly as sent (for example `/v1/people?limit=25`) and `body` is the exact JSON sent, or empty for GET. A new signature is made for each retry. Requests older than 5 minutes are refused, so keep your server clock in sync. To sign by hand, use `benvo.sign_request(secret, method, path, body, timestamp=None)`.
208
+
209
+ ## Test keys and live keys
210
+
211
+ | Key | Works on | `livemode` |
212
+ |---|---|---|
213
+ | `bvk_test_...` | Your sandbox account only, with fake data. Nothing reaches real guests | `False` |
214
+ | `bvk_live_...` | The venue account it was issued for, after the venue owner approves it | `True` |
215
+
216
+ The same code runs against both. `client.livemode` tells you which kind of key the client holds, and every object and webhook event carries `livemode`. Build and test with a test key, then swap in the live key.
217
+
218
+ ## Development
219
+
220
+ Run from `packages/sdk-python`:
221
+
222
+ ```bash
223
+ python3 -m unittest discover -s tests # offline test suite
224
+ python3 scripts/build_dist.py # dist/benvo-1.0.0.tar.gz and dist/benvo-1.0.0-py3-none-any.whl, standard library only
225
+ python3 -m build # the same archives with the official builder, when the build package is installed
226
+ ```
227
+
228
+ ## Licence
229
+
230
+ MIT. Copyright Coviola Group LLC.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "benvo"
7
+ version = "1.0.0"
8
+ description = "Official Python library for the Benvo API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Coviola Group LLC", email = "support@benvo.io" }]
13
+ keywords = ["benvo", "api", "sdk", "hospitality", "restaurants", "members clubs", "reservations", "webhooks"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.urls]
33
+ Homepage = "https://developers.benvo.io"
34
+ Documentation = "https://developers.benvo.io"
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["src"]
38
+
39
+ [tool.setuptools.package-data]
40
+ benvo = ["py.typed"]
@@ -0,0 +1,43 @@
1
+ from . import webhooks
2
+ from ._client import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, Benvo, response_of
3
+ from ._endpoints import ENDPOINTS, Endpoint
4
+ from ._pagination import ListIterator
5
+ from ._transport import BenvoObject
6
+ from ._version import __version__
7
+ from .errors import (
8
+ BenvoApiError,
9
+ BenvoAuthenticationError,
10
+ BenvoConnectionError,
11
+ BenvoError,
12
+ BenvoInvalidRequestError,
13
+ BenvoNotFoundError,
14
+ BenvoPermissionError,
15
+ BenvoRateLimitError,
16
+ BenvoWebhookVerificationError,
17
+ )
18
+ from .signing import SIGNATURE_HEADER, sign_request
19
+
20
+ __all__ = [
21
+ "DEFAULT_BASE_URL",
22
+ "DEFAULT_MAX_RETRIES",
23
+ "DEFAULT_TIMEOUT",
24
+ "ENDPOINTS",
25
+ "SIGNATURE_HEADER",
26
+ "Benvo",
27
+ "BenvoApiError",
28
+ "BenvoAuthenticationError",
29
+ "BenvoConnectionError",
30
+ "BenvoError",
31
+ "BenvoInvalidRequestError",
32
+ "BenvoNotFoundError",
33
+ "BenvoObject",
34
+ "BenvoPermissionError",
35
+ "BenvoRateLimitError",
36
+ "BenvoWebhookVerificationError",
37
+ "Endpoint",
38
+ "ListIterator",
39
+ "__version__",
40
+ "response_of",
41
+ "sign_request",
42
+ "webhooks",
43
+ ]
@@ -0,0 +1,80 @@
1
+ import re
2
+ from typing import Any, Dict, Mapping, Optional
3
+
4
+ from ._resources import (
5
+ AvailabilityResource,
6
+ Events,
7
+ Invoices,
8
+ Leads,
9
+ Members,
10
+ MembershipTiers,
11
+ Menus,
12
+ Meta,
13
+ Orders,
14
+ Payments,
15
+ People,
16
+ Products,
17
+ Reservations,
18
+ Rsvps,
19
+ )
20
+ from ._transport import Transport, default_user_agent
21
+ from ._version import __version__
22
+ from .types import Ping
23
+
24
+ DEFAULT_BASE_URL = "https://api.benvo.io"
25
+ DEFAULT_TIMEOUT = 30.0
26
+ DEFAULT_MAX_RETRIES = 2
27
+
28
+
29
+ class Benvo:
30
+ VERSION = __version__
31
+
32
+ def __init__(
33
+ self,
34
+ api_key: str,
35
+ *,
36
+ base_url: str = DEFAULT_BASE_URL,
37
+ timeout: float = DEFAULT_TIMEOUT,
38
+ max_retries: int = DEFAULT_MAX_RETRIES,
39
+ signing_secret: Optional[str] = None,
40
+ user_agent: Optional[str] = None,
41
+ ) -> None:
42
+ if not isinstance(api_key, str) or not api_key.strip():
43
+ raise ValueError("A Benvo API key is required")
44
+ base = base_url.rstrip("/")
45
+ if not re.match(r"^https?://", base):
46
+ raise ValueError("base_url must start with https:// or http://")
47
+ self.base_url = base
48
+ self.livemode = not api_key.startswith("bvk_test_")
49
+ self._transport = Transport(
50
+ api_key=api_key,
51
+ base_url=base,
52
+ timeout=timeout,
53
+ max_retries=max_retries,
54
+ signing_secret=signing_secret,
55
+ user_agent=user_agent or default_user_agent(),
56
+ )
57
+ self.people = People(self._transport)
58
+ self.members = Members(self._transport)
59
+ self.membership_tiers = MembershipTiers(self._transport)
60
+ self.leads = Leads(self._transport)
61
+ self.reservations = Reservations(self._transport)
62
+ self.availability = AvailabilityResource(self._transport)
63
+ self.events = Events(self._transport)
64
+ self.rsvps = Rsvps(self._transport)
65
+ self.orders = Orders(self._transport)
66
+ self.invoices = Invoices(self._transport)
67
+ self.payments = Payments(self._transport)
68
+ self.menus = Menus(self._transport)
69
+ self.products = Products(self._transport)
70
+ self.meta = Meta(self._transport)
71
+
72
+ def ping(self, *, request_options: Optional[Mapping[str, Any]] = None) -> Ping:
73
+ return self._transport.call("ping", **dict(request_options or {}))
74
+
75
+ def __repr__(self) -> str:
76
+ return f"Benvo(base_url={self.base_url!r}, livemode={self.livemode!r})"
77
+
78
+
79
+ def response_of(obj: Any) -> Optional[Dict[str, Any]]:
80
+ return getattr(obj, "last_response", None)