vistalink 0.0.1__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.
- vistalink-0.0.1/.gitignore +17 -0
- vistalink-0.0.1/LICENSE +21 -0
- vistalink-0.0.1/PKG-INFO +231 -0
- vistalink-0.0.1/README.md +193 -0
- vistalink-0.0.1/pyproject.toml +50 -0
- vistalink-0.0.1/src/vistalink/__init__.py +39 -0
- vistalink-0.0.1/src/vistalink/_async_client.py +267 -0
- vistalink-0.0.1/src/vistalink/_base.py +281 -0
- vistalink-0.0.1/src/vistalink/_client.py +264 -0
- vistalink-0.0.1/src/vistalink/_errors.py +79 -0
- vistalink-0.0.1/src/vistalink/_retry.py +38 -0
- vistalink-0.0.1/src/vistalink/models.py +4737 -0
- vistalink-0.0.1/src/vistalink/py.typed +0 -0
- vistalink-0.0.1/src/vistalink/webhooks.py +88 -0
vistalink-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VistaLink
|
|
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.
|
vistalink-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vistalink
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python client for the VistaLink hotel API v2 (sync + async, httpx + pydantic)
|
|
5
|
+
Project-URL: Homepage, https://vistalink.com/developers
|
|
6
|
+
Project-URL: Documentation, https://vistalink.com/developers/docs
|
|
7
|
+
Project-URL: Source, https://github.com/vistalink-technologies/vistalink-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/vistalink-technologies/vistalink-sdk/issues
|
|
9
|
+
Project-URL: OpenAPI, https://api.vistalink.com/v2/openapi.json
|
|
10
|
+
Author: VistaLink
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,hotels,sdk,travel,vistalink
|
|
14
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: httpx<1,>=0.27
|
|
28
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: datamodel-code-generator[http]>=0.26; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
33
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
34
|
+
Provides-Extra: release
|
|
35
|
+
Requires-Dist: build>=1.2; extra == 'release'
|
|
36
|
+
Requires-Dist: twine>=6.1; extra == 'release'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# vistalink (Python)
|
|
40
|
+
|
|
41
|
+
> 0.0.x reserves the name; the 1.0 release tracks the API v2 launch.
|
|
42
|
+
|
|
43
|
+
Python client for the VistaLink hotel API v2: a thin sync + async client on
|
|
44
|
+
`httpx`, with pydantic v2 models generated from `spec/openapi.v2.json`.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install vistalink
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import os
|
|
53
|
+
from vistalink import VistaLink, AsyncVistaLink, VistaLinkError, booking_next_step, parse_ndjson
|
|
54
|
+
|
|
55
|
+
vl = VistaLink(api_key=os.environ["VISTALINK_API_KEY"]) # base_url defaults to https://api.vistalink.com
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Models:** generated with `datamodel-code-generator`, chosen because it emits
|
|
59
|
+
plain pydantic v2 models and nothing else, which suits a hand-written thin
|
|
60
|
+
client better than `openapi-python-client`'s full generated client with attrs
|
|
61
|
+
models. Request bodies accept either a dict or a model
|
|
62
|
+
(`vistalink.models.SearchRequest(...)`). Responses are always parsed into
|
|
63
|
+
models (`SearchResponse`, `Job`, `Booking`, …). Unknown fields are kept
|
|
64
|
+
(`extra="allow"`).
|
|
65
|
+
|
|
66
|
+
What the client does for you, same as the TypeScript SDK:
|
|
67
|
+
|
|
68
|
+
- `Authorization: Bearer <key>` on every call.
|
|
69
|
+
- `Idempotency-Key` generated for `bookings.create`, `rates.batch`,
|
|
70
|
+
`rooms.reconcile` and live `rates`. Pass `idempotency_key=` to set it
|
|
71
|
+
yourself. Retries reuse the same key.
|
|
72
|
+
- Retries 429 (honouring `Retry-After`) and 503 with jittered exponential
|
|
73
|
+
backoff (`max_retries=3`). Network errors are retried only for GET/DELETE
|
|
74
|
+
and POSTs that carry an idempotency key.
|
|
75
|
+
- Raises `VistaLinkError(code, message, status, request_id, details)` for
|
|
76
|
+
every non-2xx. Client-side codes are `network_error`, `timeout`,
|
|
77
|
+
`job_failed` and `job_canceled`.
|
|
78
|
+
- `jobs.wait()` / `bookings.wait()`, `jobs.download()` /
|
|
79
|
+
`jobs.wait_for_result()` for `rates.batch` NDJSON (signed `result_url`, else
|
|
80
|
+
`GET /v2/jobs/{id}/result`), `booking_next_step()`, `jobs.list_all()` /
|
|
81
|
+
`search.pages()`, and `webhooks.verify()`.
|
|
82
|
+
|
|
83
|
+
## 1. Resolve identifiers (free)
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
res = vl.hotels.resolve({"identifiers": [
|
|
87
|
+
{"booking_url": "https://www.booking.com/hotel/it/umilta-36.en-gb.html"},
|
|
88
|
+
{"website_url": "https://umilta36.com"},
|
|
89
|
+
{"name": "Umiltà 36", "city": "Rome", "country": "IT"},
|
|
90
|
+
]})
|
|
91
|
+
for r in res.results:
|
|
92
|
+
print(r.input, r.status, r.hotel_id, r.match)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 2. Search, the 2×2
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# structured + filters ($0.003)
|
|
99
|
+
structured = vl.search.run({
|
|
100
|
+
"filters": {
|
|
101
|
+
"location": {"city": "Rome", "country": "IT"},
|
|
102
|
+
"stay": {"check_in": "2026-11-14", "check_out": "2026-11-16", "rooms": [{"adults": 2}]},
|
|
103
|
+
"budget": {"max": "400", "currency": "EUR"},
|
|
104
|
+
"min_stars": 4,
|
|
105
|
+
"amenities": ["SPA"],
|
|
106
|
+
},
|
|
107
|
+
"ranking": "filters",
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# loose + semantic ($0.012)
|
|
111
|
+
loose = vl.search.run({
|
|
112
|
+
"query": "quiet place near the Colosseum with a rooftop, under 400 a night",
|
|
113
|
+
"ranking": "semantic",
|
|
114
|
+
"limit": 10,
|
|
115
|
+
})
|
|
116
|
+
if loose.status == "needs_clarification":
|
|
117
|
+
q = loose.clarifications[0]
|
|
118
|
+
loose = vl.search.answer(loose.search_id, [{"clarification_id": q.id, "option_id": q.options[0].id}])
|
|
119
|
+
# query + "filters" = loose.filters ($0.005); filters only + "semantic" = structured.semantic ($0.008)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 3. Hotel detail with the search's `search_id`
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
top = loose.hotels[0]
|
|
126
|
+
hotel = vl.hotels.get(
|
|
127
|
+
top.hotel_id,
|
|
128
|
+
search_id=loose.search_id, # warmed cache, same price
|
|
129
|
+
sections="core,images,rooms,rates",
|
|
130
|
+
check_in="2026-11-14", check_out="2026-11-16", adults=2,
|
|
131
|
+
)
|
|
132
|
+
print(hotel.core.name, len(hotel.rooms or []))
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 4. Cached rates, then a live job
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
stay = {"check_in": "2026-11-14", "check_out": "2026-11-16", "rooms": [{"adults": 2}]}
|
|
139
|
+
|
|
140
|
+
cached = vl.rates.cached({"hotels": [top.hotel_id], "stay": stay, "max_age_hours": 24})
|
|
141
|
+
print(cached.summary, cached.results[0].status)
|
|
142
|
+
|
|
143
|
+
job = vl.rates.live({"hotels": [top.hotel_id], "stay": stay}) # 202 + Idempotency-Key
|
|
144
|
+
done = vl.jobs.wait(job.job_id, timeout=120, on_poll=lambda j: print(j.status, j.progress))
|
|
145
|
+
print(done.result) # same shape as the cached response
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Batch rates: the NDJSON result
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
job = vl.rates.batch({"hotels": ids, "stays": [stay]})
|
|
152
|
+
job, ndjson = vl.jobs.wait_for_result(job.job_id, timeout=1800)
|
|
153
|
+
for row in parse_ndjson(ndjson):
|
|
154
|
+
print(row["hotel_id"], row["status"])
|
|
155
|
+
# jobs.download(job) alone: a signed result_url is fetched without the key;
|
|
156
|
+
# when result_url is None it is GET /v2/jobs/{id}/result with the key (free).
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## 5. Booking (the guest always pays the property)
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
offer_id = cached.results[0].rooms[0].offers[0].offer_id
|
|
163
|
+
booking = vl.bookings.create(
|
|
164
|
+
{
|
|
165
|
+
"hotel_id": top.hotel_id,
|
|
166
|
+
"offer_id": offer_id,
|
|
167
|
+
"stay": stay,
|
|
168
|
+
"guests": [{"room": 0, "lead": {"first_name": "Ada", "last_name": "Rossi", "email": "ada@example.com"}}],
|
|
169
|
+
"payment": {"mode": "checkout", "return_url": "https://partner.example/booking/done", "locale": "en"},
|
|
170
|
+
},
|
|
171
|
+
idempotency_key=f"booking-{order_id}", # reuse on retry; omitted, one is generated
|
|
172
|
+
)
|
|
173
|
+
ready = vl.bookings.wait(booking.booking_id) # stops when the guest has to act, or final
|
|
174
|
+
step = booking_next_step(ready)
|
|
175
|
+
if step.url:
|
|
176
|
+
show_link(step.url) # checkout: payment.checkout_url; handoff / external: handoff.url
|
|
177
|
+
final = vl.bookings.wait(booking.booking_id, until=["confirmed", "confirmation_pending", "failed", "expired", "canceled"])
|
|
178
|
+
if final.status == "failed":
|
|
179
|
+
print(final.failure.reason) # e.g. payment_unavailable
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`payment.mode` (`PAYMENT_MODES`) is `checkout` (stops at `requires_payment`,
|
|
183
|
+
`payment.checkout_url`), `handoff` (stops at `requires_user_action`, hosted
|
|
184
|
+
session at `handoff.url`) or `external` (stops at `requires_user_action`, the
|
|
185
|
+
engine's own prefilled page at `handoff.url`). `bookings.cancel(id)` moves a
|
|
186
|
+
booking to `canceled`. `webhooks.delete(id)` returns `None` (204).
|
|
187
|
+
|
|
188
|
+
## Async
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
async with AsyncVistaLink(api_key=os.environ["VISTALINK_API_KEY"]) as vl:
|
|
192
|
+
res = await vl.search.run({"query": "rooftop near the Pantheon"})
|
|
193
|
+
job = await vl.rates.live({"hotels": [res.hotels[0].hotel_id], "stay": stay})
|
|
194
|
+
done = await vl.jobs.wait(job.job_id)
|
|
195
|
+
async for j in vl.jobs.list_all(kind="rates.live"):
|
|
196
|
+
...
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Errors
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
try:
|
|
203
|
+
vl.rates.cached({"hotels": ["…"], "stay": stay, "channel": "third_party"})
|
|
204
|
+
except VistaLinkError as e:
|
|
205
|
+
print(e.status, e.code, e.message, e.request_id, e.details)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Webhooks
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from vistalink import webhooks, WebhookVerificationError
|
|
212
|
+
|
|
213
|
+
@app.post("/hooks") # Flask: request.get_data(); FastAPI: await request.body()
|
|
214
|
+
def hook():
|
|
215
|
+
try:
|
|
216
|
+
event = webhooks.verify(os.environ["VISTALINK_WEBHOOK_SECRET"], request.headers, request.get_data())
|
|
217
|
+
except WebhookVerificationError:
|
|
218
|
+
return "", 400
|
|
219
|
+
...
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The tolerance is 5 minutes (`tolerance=300`). Pass the raw body, not
|
|
223
|
+
re-serialised JSON.
|
|
224
|
+
|
|
225
|
+
## Development
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
uv venv .venv && uv pip install -e '.[dev]'
|
|
229
|
+
./scripts/generate.sh # models.py + _async_client.py from ../../spec/openapi.v2.json
|
|
230
|
+
.venv/bin/pytest # respx-mocked tests, sync + async
|
|
231
|
+
```
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# vistalink (Python)
|
|
2
|
+
|
|
3
|
+
> 0.0.x reserves the name; the 1.0 release tracks the API v2 launch.
|
|
4
|
+
|
|
5
|
+
Python client for the VistaLink hotel API v2: a thin sync + async client on
|
|
6
|
+
`httpx`, with pydantic v2 models generated from `spec/openapi.v2.json`.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install vistalink
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import os
|
|
15
|
+
from vistalink import VistaLink, AsyncVistaLink, VistaLinkError, booking_next_step, parse_ndjson
|
|
16
|
+
|
|
17
|
+
vl = VistaLink(api_key=os.environ["VISTALINK_API_KEY"]) # base_url defaults to https://api.vistalink.com
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Models:** generated with `datamodel-code-generator`, chosen because it emits
|
|
21
|
+
plain pydantic v2 models and nothing else, which suits a hand-written thin
|
|
22
|
+
client better than `openapi-python-client`'s full generated client with attrs
|
|
23
|
+
models. Request bodies accept either a dict or a model
|
|
24
|
+
(`vistalink.models.SearchRequest(...)`). Responses are always parsed into
|
|
25
|
+
models (`SearchResponse`, `Job`, `Booking`, …). Unknown fields are kept
|
|
26
|
+
(`extra="allow"`).
|
|
27
|
+
|
|
28
|
+
What the client does for you, same as the TypeScript SDK:
|
|
29
|
+
|
|
30
|
+
- `Authorization: Bearer <key>` on every call.
|
|
31
|
+
- `Idempotency-Key` generated for `bookings.create`, `rates.batch`,
|
|
32
|
+
`rooms.reconcile` and live `rates`. Pass `idempotency_key=` to set it
|
|
33
|
+
yourself. Retries reuse the same key.
|
|
34
|
+
- Retries 429 (honouring `Retry-After`) and 503 with jittered exponential
|
|
35
|
+
backoff (`max_retries=3`). Network errors are retried only for GET/DELETE
|
|
36
|
+
and POSTs that carry an idempotency key.
|
|
37
|
+
- Raises `VistaLinkError(code, message, status, request_id, details)` for
|
|
38
|
+
every non-2xx. Client-side codes are `network_error`, `timeout`,
|
|
39
|
+
`job_failed` and `job_canceled`.
|
|
40
|
+
- `jobs.wait()` / `bookings.wait()`, `jobs.download()` /
|
|
41
|
+
`jobs.wait_for_result()` for `rates.batch` NDJSON (signed `result_url`, else
|
|
42
|
+
`GET /v2/jobs/{id}/result`), `booking_next_step()`, `jobs.list_all()` /
|
|
43
|
+
`search.pages()`, and `webhooks.verify()`.
|
|
44
|
+
|
|
45
|
+
## 1. Resolve identifiers (free)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
res = vl.hotels.resolve({"identifiers": [
|
|
49
|
+
{"booking_url": "https://www.booking.com/hotel/it/umilta-36.en-gb.html"},
|
|
50
|
+
{"website_url": "https://umilta36.com"},
|
|
51
|
+
{"name": "Umiltà 36", "city": "Rome", "country": "IT"},
|
|
52
|
+
]})
|
|
53
|
+
for r in res.results:
|
|
54
|
+
print(r.input, r.status, r.hotel_id, r.match)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 2. Search, the 2×2
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# structured + filters ($0.003)
|
|
61
|
+
structured = vl.search.run({
|
|
62
|
+
"filters": {
|
|
63
|
+
"location": {"city": "Rome", "country": "IT"},
|
|
64
|
+
"stay": {"check_in": "2026-11-14", "check_out": "2026-11-16", "rooms": [{"adults": 2}]},
|
|
65
|
+
"budget": {"max": "400", "currency": "EUR"},
|
|
66
|
+
"min_stars": 4,
|
|
67
|
+
"amenities": ["SPA"],
|
|
68
|
+
},
|
|
69
|
+
"ranking": "filters",
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
# loose + semantic ($0.012)
|
|
73
|
+
loose = vl.search.run({
|
|
74
|
+
"query": "quiet place near the Colosseum with a rooftop, under 400 a night",
|
|
75
|
+
"ranking": "semantic",
|
|
76
|
+
"limit": 10,
|
|
77
|
+
})
|
|
78
|
+
if loose.status == "needs_clarification":
|
|
79
|
+
q = loose.clarifications[0]
|
|
80
|
+
loose = vl.search.answer(loose.search_id, [{"clarification_id": q.id, "option_id": q.options[0].id}])
|
|
81
|
+
# query + "filters" = loose.filters ($0.005); filters only + "semantic" = structured.semantic ($0.008)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 3. Hotel detail with the search's `search_id`
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
top = loose.hotels[0]
|
|
88
|
+
hotel = vl.hotels.get(
|
|
89
|
+
top.hotel_id,
|
|
90
|
+
search_id=loose.search_id, # warmed cache, same price
|
|
91
|
+
sections="core,images,rooms,rates",
|
|
92
|
+
check_in="2026-11-14", check_out="2026-11-16", adults=2,
|
|
93
|
+
)
|
|
94
|
+
print(hotel.core.name, len(hotel.rooms or []))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 4. Cached rates, then a live job
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
stay = {"check_in": "2026-11-14", "check_out": "2026-11-16", "rooms": [{"adults": 2}]}
|
|
101
|
+
|
|
102
|
+
cached = vl.rates.cached({"hotels": [top.hotel_id], "stay": stay, "max_age_hours": 24})
|
|
103
|
+
print(cached.summary, cached.results[0].status)
|
|
104
|
+
|
|
105
|
+
job = vl.rates.live({"hotels": [top.hotel_id], "stay": stay}) # 202 + Idempotency-Key
|
|
106
|
+
done = vl.jobs.wait(job.job_id, timeout=120, on_poll=lambda j: print(j.status, j.progress))
|
|
107
|
+
print(done.result) # same shape as the cached response
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Batch rates: the NDJSON result
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
job = vl.rates.batch({"hotels": ids, "stays": [stay]})
|
|
114
|
+
job, ndjson = vl.jobs.wait_for_result(job.job_id, timeout=1800)
|
|
115
|
+
for row in parse_ndjson(ndjson):
|
|
116
|
+
print(row["hotel_id"], row["status"])
|
|
117
|
+
# jobs.download(job) alone: a signed result_url is fetched without the key;
|
|
118
|
+
# when result_url is None it is GET /v2/jobs/{id}/result with the key (free).
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 5. Booking (the guest always pays the property)
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
offer_id = cached.results[0].rooms[0].offers[0].offer_id
|
|
125
|
+
booking = vl.bookings.create(
|
|
126
|
+
{
|
|
127
|
+
"hotel_id": top.hotel_id,
|
|
128
|
+
"offer_id": offer_id,
|
|
129
|
+
"stay": stay,
|
|
130
|
+
"guests": [{"room": 0, "lead": {"first_name": "Ada", "last_name": "Rossi", "email": "ada@example.com"}}],
|
|
131
|
+
"payment": {"mode": "checkout", "return_url": "https://partner.example/booking/done", "locale": "en"},
|
|
132
|
+
},
|
|
133
|
+
idempotency_key=f"booking-{order_id}", # reuse on retry; omitted, one is generated
|
|
134
|
+
)
|
|
135
|
+
ready = vl.bookings.wait(booking.booking_id) # stops when the guest has to act, or final
|
|
136
|
+
step = booking_next_step(ready)
|
|
137
|
+
if step.url:
|
|
138
|
+
show_link(step.url) # checkout: payment.checkout_url; handoff / external: handoff.url
|
|
139
|
+
final = vl.bookings.wait(booking.booking_id, until=["confirmed", "confirmation_pending", "failed", "expired", "canceled"])
|
|
140
|
+
if final.status == "failed":
|
|
141
|
+
print(final.failure.reason) # e.g. payment_unavailable
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`payment.mode` (`PAYMENT_MODES`) is `checkout` (stops at `requires_payment`,
|
|
145
|
+
`payment.checkout_url`), `handoff` (stops at `requires_user_action`, hosted
|
|
146
|
+
session at `handoff.url`) or `external` (stops at `requires_user_action`, the
|
|
147
|
+
engine's own prefilled page at `handoff.url`). `bookings.cancel(id)` moves a
|
|
148
|
+
booking to `canceled`. `webhooks.delete(id)` returns `None` (204).
|
|
149
|
+
|
|
150
|
+
## Async
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
async with AsyncVistaLink(api_key=os.environ["VISTALINK_API_KEY"]) as vl:
|
|
154
|
+
res = await vl.search.run({"query": "rooftop near the Pantheon"})
|
|
155
|
+
job = await vl.rates.live({"hotels": [res.hotels[0].hotel_id], "stay": stay})
|
|
156
|
+
done = await vl.jobs.wait(job.job_id)
|
|
157
|
+
async for j in vl.jobs.list_all(kind="rates.live"):
|
|
158
|
+
...
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Errors
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
try:
|
|
165
|
+
vl.rates.cached({"hotels": ["…"], "stay": stay, "channel": "third_party"})
|
|
166
|
+
except VistaLinkError as e:
|
|
167
|
+
print(e.status, e.code, e.message, e.request_id, e.details)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Webhooks
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from vistalink import webhooks, WebhookVerificationError
|
|
174
|
+
|
|
175
|
+
@app.post("/hooks") # Flask: request.get_data(); FastAPI: await request.body()
|
|
176
|
+
def hook():
|
|
177
|
+
try:
|
|
178
|
+
event = webhooks.verify(os.environ["VISTALINK_WEBHOOK_SECRET"], request.headers, request.get_data())
|
|
179
|
+
except WebhookVerificationError:
|
|
180
|
+
return "", 400
|
|
181
|
+
...
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The tolerance is 5 minutes (`tolerance=300`). Pass the raw body, not
|
|
185
|
+
re-serialised JSON.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
uv venv .venv && uv pip install -e '.[dev]'
|
|
191
|
+
./scripts/generate.sh # models.py + _async_client.py from ../../spec/openapi.v2.json
|
|
192
|
+
.venv/bin/pytest # respx-mocked tests, sync + async
|
|
193
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vistalink"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Python client for the VistaLink hotel API v2 (sync + async, httpx + pydantic)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "VistaLink" }]
|
|
13
|
+
keywords = ["vistalink", "hotels", "travel", "api", "sdk"]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
dependencies = ["httpx>=0.27,<1", "pydantic>=2.6,<3"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
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
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
"Framework :: AsyncIO",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://vistalink.com/developers"
|
|
33
|
+
Documentation = "https://vistalink.com/developers/docs"
|
|
34
|
+
Source = "https://github.com/vistalink-technologies/vistalink-sdk"
|
|
35
|
+
Issues = "https://github.com/vistalink-technologies/vistalink-sdk/issues"
|
|
36
|
+
"OpenAPI" = "https://api.vistalink.com/v2/openapi.json"
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.23", "respx>=0.21", "datamodel-code-generator[http]>=0.26"]
|
|
40
|
+
release = ["build>=1.2", "twine>=6.1"] # twine 6.1+ understands Metadata-Version 2.4 (license-files)
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = ["src/vistalink", "README.md", "LICENSE", "pyproject.toml"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/vistalink"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""VistaLink hotel API v2 client (placeholder distribution name ``vistalink``)."""
|
|
2
|
+
from . import models, webhooks
|
|
3
|
+
from ._async_client import AsyncVistaLink
|
|
4
|
+
from ._base import (
|
|
5
|
+
BOOKING_FINAL,
|
|
6
|
+
BOOKING_STOP_DEFAULT,
|
|
7
|
+
DEFAULT_BASE_URL,
|
|
8
|
+
JOB_TERMINAL,
|
|
9
|
+
PAYMENT_MODES,
|
|
10
|
+
BookingNextStep,
|
|
11
|
+
__version__,
|
|
12
|
+
booking_next_step,
|
|
13
|
+
is_job,
|
|
14
|
+
parse_ndjson,
|
|
15
|
+
)
|
|
16
|
+
from ._client import VistaLink
|
|
17
|
+
from ._errors import VistaLinkError, WebhookVerificationError
|
|
18
|
+
from ._retry import RetryConfig, parse_retry_after
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"VistaLink",
|
|
22
|
+
"AsyncVistaLink",
|
|
23
|
+
"VistaLinkError",
|
|
24
|
+
"WebhookVerificationError",
|
|
25
|
+
"RetryConfig",
|
|
26
|
+
"parse_retry_after",
|
|
27
|
+
"is_job",
|
|
28
|
+
"models",
|
|
29
|
+
"webhooks",
|
|
30
|
+
"DEFAULT_BASE_URL",
|
|
31
|
+
"JOB_TERMINAL",
|
|
32
|
+
"BOOKING_STOP_DEFAULT",
|
|
33
|
+
"BOOKING_FINAL",
|
|
34
|
+
"PAYMENT_MODES",
|
|
35
|
+
"BookingNextStep",
|
|
36
|
+
"booking_next_step",
|
|
37
|
+
"parse_ndjson",
|
|
38
|
+
"__version__",
|
|
39
|
+
]
|