likyly 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.
- likyly-1.0.0/.gitignore +5 -0
- likyly-1.0.0/LICENSE +21 -0
- likyly-1.0.0/PKG-INFO +315 -0
- likyly-1.0.0/README.md +286 -0
- likyly-1.0.0/examples/quickstart.py +150 -0
- likyly-1.0.0/pyproject.toml +60 -0
- likyly-1.0.0/src/likyly/__init__.py +41 -0
- likyly-1.0.0/src/likyly/_http.py +151 -0
- likyly-1.0.0/src/likyly/_ops.py +382 -0
- likyly-1.0.0/src/likyly/_version.py +3 -0
- likyly-1.0.0/src/likyly/aresources.py +202 -0
- likyly-1.0.0/src/likyly/client.py +108 -0
- likyly-1.0.0/src/likyly/errors.py +103 -0
- likyly-1.0.0/src/likyly/models.py +129 -0
- likyly-1.0.0/src/likyly/py.typed +0 -0
- likyly-1.0.0/src/likyly/resources.py +202 -0
- likyly-1.0.0/tests/conftest.py +70 -0
- likyly-1.0.0/tests/test_behavior.py +192 -0
- likyly-1.0.0/tests/test_conformance.py +113 -0
- likyly-1.0.0/tests/test_live.py +77 -0
likyly-1.0.0/.gitignore
ADDED
likyly-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LIKYLY
|
|
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.
|
likyly-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: likyly
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Product recommendations for your site ("You may also like", "Recommended for you"), learned from what your visitors do. Official LIKYLY client for Python.
|
|
5
|
+
Project-URL: Homepage, https://www.likyly.com
|
|
6
|
+
Project-URL: Documentation, https://likyly.com/docs/sdk/python
|
|
7
|
+
Project-URL: Source, https://github.com/Likyly/likyly-recsys/tree/main/sdk/python
|
|
8
|
+
Project-URL: Issues, https://github.com/Likyly/likyly-recsys/issues
|
|
9
|
+
Author: LIKYLY
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ecommerce,likyly,personalization,product recommendations,recommendations,recommender
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx<1,>=0.27
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# LIKYLY SDK for Python
|
|
31
|
+
|
|
32
|
+
[LIKYLY](https://likyly.com) shows each visitor of your site the products they are most likely to want:
|
|
33
|
+
"You may also like" on a product page, "Recommended for you" on the home page, "Complete your order" in
|
|
34
|
+
the cart. It learns from what visitors actually do, and gets better as they do it.
|
|
35
|
+
|
|
36
|
+
This is the official Python client for the LIKYLY API. Your code uses it to:
|
|
37
|
+
|
|
38
|
+
1. **Send your catalog**: the products, articles or films you want recommended.
|
|
39
|
+
2. **Report what visitors do**: they view a product, add it to their cart, sometimes buy it. Browsing
|
|
40
|
+
alone is useful: a visitor who never buys still teaches LIKYLY what goes together, and still gets
|
|
41
|
+
recommendations.
|
|
42
|
+
3. **Ask what to show**: the products to recommend on a page, for a visitor, or both.
|
|
43
|
+
4. **Report what happened to the recommendations**: which ones were shown and which were clicked. LIKYLY
|
|
44
|
+
learns which recommendations work, and you can measure what they bring in.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pip install likyly
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Python 3.10 or later. Depends on `httpx`. Fully typed (`py.typed`). Sync (`Likyly`) and asyncio (`AsyncLikyly`) clients with the same methods.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
### Initialize
|
|
57
|
+
|
|
58
|
+
Create one client and reuse it. Use your **secret** key on your server (see [Which key?](#which-key)).
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import os
|
|
62
|
+
|
|
63
|
+
from likyly import Likyly, NotFoundError, RateLimitError
|
|
64
|
+
|
|
65
|
+
# Backend only: the secret key gives access to your catalog and your users.
|
|
66
|
+
likyly = Likyly(
|
|
67
|
+
api_key=os.environ["LIKYLY_SECRET_KEY"],
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Send your catalog
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# Send your catalog once (and whenever it changes). Ids are YOUR ids: SKUs, UUIDs, Shopify gids...
|
|
75
|
+
likyly.items.upsert_many([
|
|
76
|
+
{"item_id": "SKU-1", "title": "Nike Air Max", "description": "Running shoe with air cushioning",
|
|
77
|
+
"properties": {"category": "shoes", "brand": "Nike", "price": 129.9}},
|
|
78
|
+
{"item_id": "SKU-2", "title": "Adidas Ultraboost", "description": "Responsive running shoe",
|
|
79
|
+
"properties": {"category": "shoes", "brand": "Adidas", "price": 149}},
|
|
80
|
+
{"item_id": "SKU-3", "title": "Nike Pegasus", "description": "Everyday running shoe",
|
|
81
|
+
"properties": {"category": "shoes", "brand": "Nike", "price": 119}},
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
# Users are optional: describe them if you want the profile to travel with their events.
|
|
85
|
+
likyly.users.upsert("user_123", properties={"country": "FR", "segment": "premium"})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Ids are your own: any string (`SKU-123`, a UUID, `gid://shopify/Product/123`). `properties` is free-form; `category` and `description` improve similarity, everything else is stored as is.
|
|
89
|
+
|
|
90
|
+
### Track events
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Tell LIKYLY what your visitors do.
|
|
94
|
+
likyly.events.view(user_id="user_123", item_id="SKU-1")
|
|
95
|
+
|
|
96
|
+
# Purchases carry an event_id: replaying the call can never count the sale twice.
|
|
97
|
+
likyly.events.purchase(
|
|
98
|
+
event_id="purchase_order_9281_SKU-1",
|
|
99
|
+
user_id="user_123",
|
|
100
|
+
item_id="SKU-1",
|
|
101
|
+
quantity=1,
|
|
102
|
+
properties={"price": 129.9, "currency": "EUR", "orderId": "order_9281"},
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Get recommendations
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# Ask what to show. You never pick an algorithm: LIKYLY chooses the best strategy for what it knows.
|
|
110
|
+
recs = likyly.recommendations.get(user_id="user_123", placement="homepage", limit=3)
|
|
111
|
+
|
|
112
|
+
print(f"strategy: {recs.strategy}")
|
|
113
|
+
for item in recs.items:
|
|
114
|
+
print(item.item_id, item.title)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
You never choose an algorithm: LIKYLY picks the best strategy for what it knows about the visitor and tells you which one it used (`strategy`: `hybrid`, `content`, `collaborative`, `session` or `popular`). With no user, no session and no item you still get a useful answer: what is popular.
|
|
118
|
+
|
|
119
|
+
### Placements: the recommended way
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
# The recommended way: a placement ("pdp-related"...) is configured once in LIKYLY - strategy,
|
|
123
|
+
# fallback, filters - and your page only says where it is and what it knows.
|
|
124
|
+
related = likyly.placements.recommend("pdp-related", context={"item_id": "SKU-123", "session_id": "sess_abc"}, limit=4)
|
|
125
|
+
print(f"strategy used: {related.strategy_used}")
|
|
126
|
+
|
|
127
|
+
# When an anonymous visitor signs in, attach their session's history to their account.
|
|
128
|
+
linked = likyly.events.identify(user_id="user_123", session_id="sess_abc")
|
|
129
|
+
print(f"linked: {linked.linked_interactions}")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A placement (`pdp-related`, `home-for-you`, `cart-cross-sell`...) is configured once in LIKYLY, by your coding assistant through the MCP server or in the console: strategy, fallback, filters, how many items. Your code only names it and passes what it knows (`itemId`, `userId`, `sessionId`, ...); change the configuration later without redeploying. `identify` attaches an anonymous visitor's history to their account the moment they sign in, so their recommendations stay personal.
|
|
133
|
+
|
|
134
|
+
### Close the loop
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# Send the recommendation_id back: LIKYLY measures which recommendations get seen, clicked and bought.
|
|
138
|
+
likyly.events.impression(user_id="user_123", item_id=recs.items[0].item_id, recommendation_id=recs.recommendation_id, placement="homepage")
|
|
139
|
+
likyly.events.click(user_id="user_123", item_id=recs.items[0].item_id, recommendation_id=recs.recommendation_id, placement="homepage")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Send the recommendation id back on the events that follow (`impression` when it is displayed, `click`, `add_to_cart`, `purchase`) and LIKYLY can measure which recommendations actually convert.
|
|
143
|
+
|
|
144
|
+
## Which key?
|
|
145
|
+
|
|
146
|
+
| | Secret key | Public key |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| Where | your server only | anywhere, including a web page |
|
|
149
|
+
| Recommendations | yes | yes |
|
|
150
|
+
| Record events | yes | yes |
|
|
151
|
+
| Items and users (create, read, delete) | yes | no (`PermissionDeniedError`) |
|
|
152
|
+
| `debug` explanations | yes | no |
|
|
153
|
+
|
|
154
|
+
The public key can only read recommendations and write events, so it is safe to expose. **Never put the secret key in code that ships to a browser or a mobile app.**
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Where the code is exposed to visitors, use the PUBLIC key: it can only read recommendations and record events.
|
|
158
|
+
front = Likyly(
|
|
159
|
+
api_key=os.environ["LIKYLY_PUBLIC_KEY"],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# A visitor who is not logged in: use an anonymous session id (any string you generate and keep in a cookie).
|
|
163
|
+
front.events.view(session_id="sess_abc", item_id="SKU-2")
|
|
164
|
+
for_visitor = front.recommendations.get(session_id="sess_abc", viewed_item_ids=["SKU-2"], placement="product_page", limit=3)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Events
|
|
168
|
+
|
|
169
|
+
`track` is the one mechanism; `impression`, `view`, `click`, `add_to_cart`, `remove_from_cart` and `purchase` are shortcuts that call it with the matching type. Event types are **open strings**: use your own (`favorite`, `share`, `video_played`, ...) as freely as the official ones.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
# Any string is a valid event type: track what matters to your business.
|
|
173
|
+
likyly.events.track("favorite", user_id="user_123", item_id="SKU-2", properties={"list": "wishlist"})
|
|
174
|
+
|
|
175
|
+
# Up to 1000 events per call, each with its own type.
|
|
176
|
+
likyly.events.track_many([
|
|
177
|
+
{"type": "view", "user_id": "user_123", "item_id": "SKU-3"},
|
|
178
|
+
{"type": "add_to_cart", "user_id": "user_123", "item_id": "SKU-3", "quantity": 1},
|
|
179
|
+
])
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- An event needs an item and a user, a session, or both (both ties the anonymous session to the user once they log in).
|
|
183
|
+
- Anonymous visitors: generate a session id (any string) and keep it in a cookie; no login needed.
|
|
184
|
+
- `properties` is a free-form object (price, currency, order id, ...). Its keys are stored exactly as you send them and are never renamed by the SDK.
|
|
185
|
+
- `event_id` (idempotency key): replaying an event with the same id records nothing and returns `duplicate: true`. Set it on purchases, for example `purchase_<orderId>_<itemId>`.
|
|
186
|
+
- `placement` is a free-form label of where something happened or will be displayed (`homepage`, `product_page`, `cart`, ...).
|
|
187
|
+
|
|
188
|
+
## Advanced recommendations
|
|
189
|
+
|
|
190
|
+
`recommendations.get` is the normal way. When you want to pick the strategy yourself:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# Advanced Recommendations: one strategy at a time, when you want to choose.
|
|
194
|
+
similar = likyly.recommendations.similar(item_id="SKU-1", limit=3)
|
|
195
|
+
hybrid = likyly.recommendations.hybrid(user_id="user_123", item_id="SKU-1", alpha=0.7, limit=3)
|
|
196
|
+
session = likyly.recommendations.session(viewed_item_ids=["SKU-1", "SKU-2"], limit=3)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
| Strategy | What it does |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `popular` | The most popular items: the fallback for a visitor with no history |
|
|
202
|
+
| `similar` | Items similar to one item (content similarity), no user needed |
|
|
203
|
+
| `collaborative` | What users with similar histories liked. Needs a trained model, otherwise a not-found error |
|
|
204
|
+
| `hybrid` | Similar items personalised for a user; `alpha` (0 to 1) weighs collaborative against content |
|
|
205
|
+
| `session` | Recency-weighted, from `viewed item ids` or from a user's own view history (exactly one) |
|
|
206
|
+
|
|
207
|
+
These endpoints carry ids in the URL path, so ids containing `/` (such as `gid://shopify/Product/1`) or, for the session list, `,` are refused with a validation error: use `recommendations.get`, which takes ids in the request body.
|
|
208
|
+
|
|
209
|
+
## Errors
|
|
210
|
+
|
|
211
|
+
Errors are typed (every one is a `LikylyError`). Catch the specific ones you care about:
|
|
212
|
+
|
|
213
|
+
| Error | When |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `ValidationError` | The request is invalid: caught by the SDK before anything is sent, or a 422 from the API (the message names the offending fields) |
|
|
216
|
+
| `AuthenticationError` | 401: missing, invalid or revoked API key |
|
|
217
|
+
| `PermissionDeniedError` | 403: for example a public key used for a secret-key operation, or a plan limit |
|
|
218
|
+
| `NotFoundError` | 404 |
|
|
219
|
+
| `RateLimitError` | 429: see the retry-after value |
|
|
220
|
+
| `ApiError` | Any other error status from the API (the four rows above are its subtypes) |
|
|
221
|
+
| `NetworkError` | No HTTP response: DNS, connection reset, TLS |
|
|
222
|
+
| `RequestTimeoutError` | The request timed out |
|
|
223
|
+
|
|
224
|
+
Errors that come from the API carry `.status_code`, `.request_id`, `.retry_after` (seconds), `.body`. Quote the request id when you contact support.
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
try:
|
|
228
|
+
likyly.items.get("does-not-exist")
|
|
229
|
+
except NotFoundError as error:
|
|
230
|
+
print("no such item, request", error.request_id)
|
|
231
|
+
except RateLimitError as error:
|
|
232
|
+
print("slow down, retry in", error.retry_after, "s")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Retries and timeouts
|
|
236
|
+
|
|
237
|
+
The SDK retries transient failures for you (2 retries by default, exponential backoff from 0.5 s capped at 8 s, with full jitter):
|
|
238
|
+
|
|
239
|
+
- **429**: always retried, honouring `Retry-After`. A `Retry-After` longer than 60 s is not waited for: the rate-limit error is returned instead.
|
|
240
|
+
- **502, 503, 504, dropped connections and timeouts**: retried only when replaying the call is harmless: reads, upserts, deletes, events that carry an `event_id`, and batches where every event has one.
|
|
241
|
+
- An event **without** an `event_id` is never retried after an ambiguous failure, so a flaky network can never record a purchase twice. Set `event_id` on events you care about and they become safe to retry.
|
|
242
|
+
- Other 4xx errors are never retried.
|
|
243
|
+
|
|
244
|
+
## Configuration
|
|
245
|
+
|
|
246
|
+
| Setting | Option | Default |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| API key | `api_key` | required |
|
|
249
|
+
| Base URL | `base_url` | `https://api.likyly.com` |
|
|
250
|
+
| Catalog | `catalog` | your only catalog |
|
|
251
|
+
| Timeout | `timeout` (seconds) | 10 |
|
|
252
|
+
| Retries | `max_retries` | 2 |
|
|
253
|
+
| User-Agent suffix | `user_agent` | none |
|
|
254
|
+
| Custom HTTP | `http_client` (an `httpx.Client` / `httpx.AsyncClient`) | a new client |
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
tuned = Likyly(
|
|
258
|
+
api_key=os.environ["LIKYLY_SECRET_KEY"],
|
|
259
|
+
timeout=5.0, # seconds per attempt (default 10)
|
|
260
|
+
max_retries=3, # automatic retries on 429 / 502 / 503 / 504 / network errors (default 2, 0 disables)
|
|
261
|
+
user_agent="my-shop/1.4", # appended to the SDK's User-Agent
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Every method takes a keyword-only `options=RequestOptions(timeout=2.0, max_retries=0)`.
|
|
266
|
+
|
|
267
|
+
## API reference
|
|
268
|
+
|
|
269
|
+
`key` says which API key may call it: **S** = secret key only, **P** = public or secret key.
|
|
270
|
+
|
|
271
|
+
| Method | What it does | Key |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `likyly.items.get` | One item by your own id | S |
|
|
274
|
+
| `likyly.items.list` | One page of the catalog (`limit` / `offset`), with the catalog's total | S |
|
|
275
|
+
| `likyly.items.upsert` | Create or replace an item (idempotent) | S |
|
|
276
|
+
| `likyly.items.delete` | Remove an item (events already recorded for it are kept) | S |
|
|
277
|
+
| `likyly.items.upsert_many` | Batch upsert, 1 to 1000 items | S |
|
|
278
|
+
| `likyly.items.import_` | Alias of the batch upsert above (JSON batch) | S |
|
|
279
|
+
| `likyly.items.delete_many` | Batch delete, 1 to 1000 ids; unknown ids are reported, not raised | S |
|
|
280
|
+
| `likyly.users.get` | One user profile | S |
|
|
281
|
+
| `likyly.users.list` | One page of users (`limit` / `offset`) | S |
|
|
282
|
+
| `likyly.users.upsert` | Create or replace a profile (free-form `properties`) | S |
|
|
283
|
+
| `likyly.users.delete` | Erase the profile **and every event recorded for that user** | S |
|
|
284
|
+
| `likyly.users.import_` | Batch upsert of users, 1 to 1000 | S |
|
|
285
|
+
| `likyly.events.track` | Record an event of any type (open string) | P |
|
|
286
|
+
| `likyly.events.track_many` | Up to 1000 events in one call, each with its own type | P |
|
|
287
|
+
| `likyly.events.impression` | The item was displayed to the visitor | P |
|
|
288
|
+
| `likyly.events.view` | The visitor looked at the item | P |
|
|
289
|
+
| `likyly.events.click` | The visitor clicked the item | P |
|
|
290
|
+
| `likyly.events.add_to_cart` | The visitor added the item to their cart | P |
|
|
291
|
+
| `likyly.events.remove_from_cart` | The visitor removed the item from their cart | P |
|
|
292
|
+
| `likyly.events.purchase` | The visitor bought the item (set an event id) | P |
|
|
293
|
+
| `likyly.events.identify` | An anonymous visitor signed in: attach their session's history to their user id | P |
|
|
294
|
+
| `likyly.placements.recommend` | Recommendations for a named placement configured in LIKYLY (the recommended way) | P |
|
|
295
|
+
| `likyly.recommendations.get` | Recommendations with the automatic strategy: user, session, item or viewed items | P |
|
|
296
|
+
| `likyly.recommendations.popular` | Advanced: the most popular items | P |
|
|
297
|
+
| `likyly.recommendations.similar` | Advanced: items similar to one item | P |
|
|
298
|
+
| `likyly.recommendations.collaborative` | Advanced: what similar users liked (needs a trained model) | P |
|
|
299
|
+
| `likyly.recommendations.hybrid` | Advanced: similar items personalised for a user (`alpha`) | P |
|
|
300
|
+
| `likyly.recommendations.session` | Advanced: from viewed items or a user's view history | P |
|
|
301
|
+
|
|
302
|
+
Lists are paginated with `limit` and `offset` (the API's default page is 100 items); the total is reported next to the page. Users have a batch `import` but no batch delete: the API has no such endpoint. `import` on items is an alias of the batch upsert (the CSV import is a dashboard feature).
|
|
303
|
+
|
|
304
|
+
## Notes for Python
|
|
305
|
+
|
|
306
|
+
- Async code: `async with AsyncLikyly(api_key=...) as likyly:` then `await likyly.recommendations.get(...)`. Use `with Likyly(...) as likyly:` (or call `close()`) to release the connection pool.
|
|
307
|
+
- `import` is a Python keyword, so the batch import is `items.import_(...)` / `users.import_(...)`. Fields are `snake_case` (`item_id`, `recommendation_id`); the keys inside `properties` are yours and are never renamed.
|
|
308
|
+
|
|
309
|
+
## Documentation and support
|
|
310
|
+
|
|
311
|
+
- Documentation: <https://likyly.com/docs/sdk/python>
|
|
312
|
+
- REST API reference (OpenAPI): <https://api.likyly.com/docs>
|
|
313
|
+
- Package: likyly on PyPI
|
|
314
|
+
|
|
315
|
+
MIT licensed.
|
likyly-1.0.0/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# LIKYLY SDK for Python
|
|
2
|
+
|
|
3
|
+
[LIKYLY](https://likyly.com) shows each visitor of your site the products they are most likely to want:
|
|
4
|
+
"You may also like" on a product page, "Recommended for you" on the home page, "Complete your order" in
|
|
5
|
+
the cart. It learns from what visitors actually do, and gets better as they do it.
|
|
6
|
+
|
|
7
|
+
This is the official Python client for the LIKYLY API. Your code uses it to:
|
|
8
|
+
|
|
9
|
+
1. **Send your catalog**: the products, articles or films you want recommended.
|
|
10
|
+
2. **Report what visitors do**: they view a product, add it to their cart, sometimes buy it. Browsing
|
|
11
|
+
alone is useful: a visitor who never buys still teaches LIKYLY what goes together, and still gets
|
|
12
|
+
recommendations.
|
|
13
|
+
3. **Ask what to show**: the products to recommend on a page, for a visitor, or both.
|
|
14
|
+
4. **Report what happened to the recommendations**: which ones were shown and which were clicked. LIKYLY
|
|
15
|
+
learns which recommendations work, and you can measure what they bring in.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pip install likyly
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Python 3.10 or later. Depends on `httpx`. Fully typed (`py.typed`). Sync (`Likyly`) and asyncio (`AsyncLikyly`) clients with the same methods.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
### Initialize
|
|
28
|
+
|
|
29
|
+
Create one client and reuse it. Use your **secret** key on your server (see [Which key?](#which-key)).
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import os
|
|
33
|
+
|
|
34
|
+
from likyly import Likyly, NotFoundError, RateLimitError
|
|
35
|
+
|
|
36
|
+
# Backend only: the secret key gives access to your catalog and your users.
|
|
37
|
+
likyly = Likyly(
|
|
38
|
+
api_key=os.environ["LIKYLY_SECRET_KEY"],
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Send your catalog
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# Send your catalog once (and whenever it changes). Ids are YOUR ids: SKUs, UUIDs, Shopify gids...
|
|
46
|
+
likyly.items.upsert_many([
|
|
47
|
+
{"item_id": "SKU-1", "title": "Nike Air Max", "description": "Running shoe with air cushioning",
|
|
48
|
+
"properties": {"category": "shoes", "brand": "Nike", "price": 129.9}},
|
|
49
|
+
{"item_id": "SKU-2", "title": "Adidas Ultraboost", "description": "Responsive running shoe",
|
|
50
|
+
"properties": {"category": "shoes", "brand": "Adidas", "price": 149}},
|
|
51
|
+
{"item_id": "SKU-3", "title": "Nike Pegasus", "description": "Everyday running shoe",
|
|
52
|
+
"properties": {"category": "shoes", "brand": "Nike", "price": 119}},
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
# Users are optional: describe them if you want the profile to travel with their events.
|
|
56
|
+
likyly.users.upsert("user_123", properties={"country": "FR", "segment": "premium"})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Ids are your own: any string (`SKU-123`, a UUID, `gid://shopify/Product/123`). `properties` is free-form; `category` and `description` improve similarity, everything else is stored as is.
|
|
60
|
+
|
|
61
|
+
### Track events
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# Tell LIKYLY what your visitors do.
|
|
65
|
+
likyly.events.view(user_id="user_123", item_id="SKU-1")
|
|
66
|
+
|
|
67
|
+
# Purchases carry an event_id: replaying the call can never count the sale twice.
|
|
68
|
+
likyly.events.purchase(
|
|
69
|
+
event_id="purchase_order_9281_SKU-1",
|
|
70
|
+
user_id="user_123",
|
|
71
|
+
item_id="SKU-1",
|
|
72
|
+
quantity=1,
|
|
73
|
+
properties={"price": 129.9, "currency": "EUR", "orderId": "order_9281"},
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Get recommendations
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# Ask what to show. You never pick an algorithm: LIKYLY chooses the best strategy for what it knows.
|
|
81
|
+
recs = likyly.recommendations.get(user_id="user_123", placement="homepage", limit=3)
|
|
82
|
+
|
|
83
|
+
print(f"strategy: {recs.strategy}")
|
|
84
|
+
for item in recs.items:
|
|
85
|
+
print(item.item_id, item.title)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
You never choose an algorithm: LIKYLY picks the best strategy for what it knows about the visitor and tells you which one it used (`strategy`: `hybrid`, `content`, `collaborative`, `session` or `popular`). With no user, no session and no item you still get a useful answer: what is popular.
|
|
89
|
+
|
|
90
|
+
### Placements: the recommended way
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# The recommended way: a placement ("pdp-related"...) is configured once in LIKYLY - strategy,
|
|
94
|
+
# fallback, filters - and your page only says where it is and what it knows.
|
|
95
|
+
related = likyly.placements.recommend("pdp-related", context={"item_id": "SKU-123", "session_id": "sess_abc"}, limit=4)
|
|
96
|
+
print(f"strategy used: {related.strategy_used}")
|
|
97
|
+
|
|
98
|
+
# When an anonymous visitor signs in, attach their session's history to their account.
|
|
99
|
+
linked = likyly.events.identify(user_id="user_123", session_id="sess_abc")
|
|
100
|
+
print(f"linked: {linked.linked_interactions}")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A placement (`pdp-related`, `home-for-you`, `cart-cross-sell`...) is configured once in LIKYLY, by your coding assistant through the MCP server or in the console: strategy, fallback, filters, how many items. Your code only names it and passes what it knows (`itemId`, `userId`, `sessionId`, ...); change the configuration later without redeploying. `identify` attaches an anonymous visitor's history to their account the moment they sign in, so their recommendations stay personal.
|
|
104
|
+
|
|
105
|
+
### Close the loop
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# Send the recommendation_id back: LIKYLY measures which recommendations get seen, clicked and bought.
|
|
109
|
+
likyly.events.impression(user_id="user_123", item_id=recs.items[0].item_id, recommendation_id=recs.recommendation_id, placement="homepage")
|
|
110
|
+
likyly.events.click(user_id="user_123", item_id=recs.items[0].item_id, recommendation_id=recs.recommendation_id, placement="homepage")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Send the recommendation id back on the events that follow (`impression` when it is displayed, `click`, `add_to_cart`, `purchase`) and LIKYLY can measure which recommendations actually convert.
|
|
114
|
+
|
|
115
|
+
## Which key?
|
|
116
|
+
|
|
117
|
+
| | Secret key | Public key |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| Where | your server only | anywhere, including a web page |
|
|
120
|
+
| Recommendations | yes | yes |
|
|
121
|
+
| Record events | yes | yes |
|
|
122
|
+
| Items and users (create, read, delete) | yes | no (`PermissionDeniedError`) |
|
|
123
|
+
| `debug` explanations | yes | no |
|
|
124
|
+
|
|
125
|
+
The public key can only read recommendations and write events, so it is safe to expose. **Never put the secret key in code that ships to a browser or a mobile app.**
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# Where the code is exposed to visitors, use the PUBLIC key: it can only read recommendations and record events.
|
|
129
|
+
front = Likyly(
|
|
130
|
+
api_key=os.environ["LIKYLY_PUBLIC_KEY"],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# A visitor who is not logged in: use an anonymous session id (any string you generate and keep in a cookie).
|
|
134
|
+
front.events.view(session_id="sess_abc", item_id="SKU-2")
|
|
135
|
+
for_visitor = front.recommendations.get(session_id="sess_abc", viewed_item_ids=["SKU-2"], placement="product_page", limit=3)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Events
|
|
139
|
+
|
|
140
|
+
`track` is the one mechanism; `impression`, `view`, `click`, `add_to_cart`, `remove_from_cart` and `purchase` are shortcuts that call it with the matching type. Event types are **open strings**: use your own (`favorite`, `share`, `video_played`, ...) as freely as the official ones.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
# Any string is a valid event type: track what matters to your business.
|
|
144
|
+
likyly.events.track("favorite", user_id="user_123", item_id="SKU-2", properties={"list": "wishlist"})
|
|
145
|
+
|
|
146
|
+
# Up to 1000 events per call, each with its own type.
|
|
147
|
+
likyly.events.track_many([
|
|
148
|
+
{"type": "view", "user_id": "user_123", "item_id": "SKU-3"},
|
|
149
|
+
{"type": "add_to_cart", "user_id": "user_123", "item_id": "SKU-3", "quantity": 1},
|
|
150
|
+
])
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- An event needs an item and a user, a session, or both (both ties the anonymous session to the user once they log in).
|
|
154
|
+
- Anonymous visitors: generate a session id (any string) and keep it in a cookie; no login needed.
|
|
155
|
+
- `properties` is a free-form object (price, currency, order id, ...). Its keys are stored exactly as you send them and are never renamed by the SDK.
|
|
156
|
+
- `event_id` (idempotency key): replaying an event with the same id records nothing and returns `duplicate: true`. Set it on purchases, for example `purchase_<orderId>_<itemId>`.
|
|
157
|
+
- `placement` is a free-form label of where something happened or will be displayed (`homepage`, `product_page`, `cart`, ...).
|
|
158
|
+
|
|
159
|
+
## Advanced recommendations
|
|
160
|
+
|
|
161
|
+
`recommendations.get` is the normal way. When you want to pick the strategy yourself:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
# Advanced Recommendations: one strategy at a time, when you want to choose.
|
|
165
|
+
similar = likyly.recommendations.similar(item_id="SKU-1", limit=3)
|
|
166
|
+
hybrid = likyly.recommendations.hybrid(user_id="user_123", item_id="SKU-1", alpha=0.7, limit=3)
|
|
167
|
+
session = likyly.recommendations.session(viewed_item_ids=["SKU-1", "SKU-2"], limit=3)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
| Strategy | What it does |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `popular` | The most popular items: the fallback for a visitor with no history |
|
|
173
|
+
| `similar` | Items similar to one item (content similarity), no user needed |
|
|
174
|
+
| `collaborative` | What users with similar histories liked. Needs a trained model, otherwise a not-found error |
|
|
175
|
+
| `hybrid` | Similar items personalised for a user; `alpha` (0 to 1) weighs collaborative against content |
|
|
176
|
+
| `session` | Recency-weighted, from `viewed item ids` or from a user's own view history (exactly one) |
|
|
177
|
+
|
|
178
|
+
These endpoints carry ids in the URL path, so ids containing `/` (such as `gid://shopify/Product/1`) or, for the session list, `,` are refused with a validation error: use `recommendations.get`, which takes ids in the request body.
|
|
179
|
+
|
|
180
|
+
## Errors
|
|
181
|
+
|
|
182
|
+
Errors are typed (every one is a `LikylyError`). Catch the specific ones you care about:
|
|
183
|
+
|
|
184
|
+
| Error | When |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `ValidationError` | The request is invalid: caught by the SDK before anything is sent, or a 422 from the API (the message names the offending fields) |
|
|
187
|
+
| `AuthenticationError` | 401: missing, invalid or revoked API key |
|
|
188
|
+
| `PermissionDeniedError` | 403: for example a public key used for a secret-key operation, or a plan limit |
|
|
189
|
+
| `NotFoundError` | 404 |
|
|
190
|
+
| `RateLimitError` | 429: see the retry-after value |
|
|
191
|
+
| `ApiError` | Any other error status from the API (the four rows above are its subtypes) |
|
|
192
|
+
| `NetworkError` | No HTTP response: DNS, connection reset, TLS |
|
|
193
|
+
| `RequestTimeoutError` | The request timed out |
|
|
194
|
+
|
|
195
|
+
Errors that come from the API carry `.status_code`, `.request_id`, `.retry_after` (seconds), `.body`. Quote the request id when you contact support.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
try:
|
|
199
|
+
likyly.items.get("does-not-exist")
|
|
200
|
+
except NotFoundError as error:
|
|
201
|
+
print("no such item, request", error.request_id)
|
|
202
|
+
except RateLimitError as error:
|
|
203
|
+
print("slow down, retry in", error.retry_after, "s")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Retries and timeouts
|
|
207
|
+
|
|
208
|
+
The SDK retries transient failures for you (2 retries by default, exponential backoff from 0.5 s capped at 8 s, with full jitter):
|
|
209
|
+
|
|
210
|
+
- **429**: always retried, honouring `Retry-After`. A `Retry-After` longer than 60 s is not waited for: the rate-limit error is returned instead.
|
|
211
|
+
- **502, 503, 504, dropped connections and timeouts**: retried only when replaying the call is harmless: reads, upserts, deletes, events that carry an `event_id`, and batches where every event has one.
|
|
212
|
+
- An event **without** an `event_id` is never retried after an ambiguous failure, so a flaky network can never record a purchase twice. Set `event_id` on events you care about and they become safe to retry.
|
|
213
|
+
- Other 4xx errors are never retried.
|
|
214
|
+
|
|
215
|
+
## Configuration
|
|
216
|
+
|
|
217
|
+
| Setting | Option | Default |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| API key | `api_key` | required |
|
|
220
|
+
| Base URL | `base_url` | `https://api.likyly.com` |
|
|
221
|
+
| Catalog | `catalog` | your only catalog |
|
|
222
|
+
| Timeout | `timeout` (seconds) | 10 |
|
|
223
|
+
| Retries | `max_retries` | 2 |
|
|
224
|
+
| User-Agent suffix | `user_agent` | none |
|
|
225
|
+
| Custom HTTP | `http_client` (an `httpx.Client` / `httpx.AsyncClient`) | a new client |
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
tuned = Likyly(
|
|
229
|
+
api_key=os.environ["LIKYLY_SECRET_KEY"],
|
|
230
|
+
timeout=5.0, # seconds per attempt (default 10)
|
|
231
|
+
max_retries=3, # automatic retries on 429 / 502 / 503 / 504 / network errors (default 2, 0 disables)
|
|
232
|
+
user_agent="my-shop/1.4", # appended to the SDK's User-Agent
|
|
233
|
+
)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Every method takes a keyword-only `options=RequestOptions(timeout=2.0, max_retries=0)`.
|
|
237
|
+
|
|
238
|
+
## API reference
|
|
239
|
+
|
|
240
|
+
`key` says which API key may call it: **S** = secret key only, **P** = public or secret key.
|
|
241
|
+
|
|
242
|
+
| Method | What it does | Key |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| `likyly.items.get` | One item by your own id | S |
|
|
245
|
+
| `likyly.items.list` | One page of the catalog (`limit` / `offset`), with the catalog's total | S |
|
|
246
|
+
| `likyly.items.upsert` | Create or replace an item (idempotent) | S |
|
|
247
|
+
| `likyly.items.delete` | Remove an item (events already recorded for it are kept) | S |
|
|
248
|
+
| `likyly.items.upsert_many` | Batch upsert, 1 to 1000 items | S |
|
|
249
|
+
| `likyly.items.import_` | Alias of the batch upsert above (JSON batch) | S |
|
|
250
|
+
| `likyly.items.delete_many` | Batch delete, 1 to 1000 ids; unknown ids are reported, not raised | S |
|
|
251
|
+
| `likyly.users.get` | One user profile | S |
|
|
252
|
+
| `likyly.users.list` | One page of users (`limit` / `offset`) | S |
|
|
253
|
+
| `likyly.users.upsert` | Create or replace a profile (free-form `properties`) | S |
|
|
254
|
+
| `likyly.users.delete` | Erase the profile **and every event recorded for that user** | S |
|
|
255
|
+
| `likyly.users.import_` | Batch upsert of users, 1 to 1000 | S |
|
|
256
|
+
| `likyly.events.track` | Record an event of any type (open string) | P |
|
|
257
|
+
| `likyly.events.track_many` | Up to 1000 events in one call, each with its own type | P |
|
|
258
|
+
| `likyly.events.impression` | The item was displayed to the visitor | P |
|
|
259
|
+
| `likyly.events.view` | The visitor looked at the item | P |
|
|
260
|
+
| `likyly.events.click` | The visitor clicked the item | P |
|
|
261
|
+
| `likyly.events.add_to_cart` | The visitor added the item to their cart | P |
|
|
262
|
+
| `likyly.events.remove_from_cart` | The visitor removed the item from their cart | P |
|
|
263
|
+
| `likyly.events.purchase` | The visitor bought the item (set an event id) | P |
|
|
264
|
+
| `likyly.events.identify` | An anonymous visitor signed in: attach their session's history to their user id | P |
|
|
265
|
+
| `likyly.placements.recommend` | Recommendations for a named placement configured in LIKYLY (the recommended way) | P |
|
|
266
|
+
| `likyly.recommendations.get` | Recommendations with the automatic strategy: user, session, item or viewed items | P |
|
|
267
|
+
| `likyly.recommendations.popular` | Advanced: the most popular items | P |
|
|
268
|
+
| `likyly.recommendations.similar` | Advanced: items similar to one item | P |
|
|
269
|
+
| `likyly.recommendations.collaborative` | Advanced: what similar users liked (needs a trained model) | P |
|
|
270
|
+
| `likyly.recommendations.hybrid` | Advanced: similar items personalised for a user (`alpha`) | P |
|
|
271
|
+
| `likyly.recommendations.session` | Advanced: from viewed items or a user's view history | P |
|
|
272
|
+
|
|
273
|
+
Lists are paginated with `limit` and `offset` (the API's default page is 100 items); the total is reported next to the page. Users have a batch `import` but no batch delete: the API has no such endpoint. `import` on items is an alias of the batch upsert (the CSV import is a dashboard feature).
|
|
274
|
+
|
|
275
|
+
## Notes for Python
|
|
276
|
+
|
|
277
|
+
- Async code: `async with AsyncLikyly(api_key=...) as likyly:` then `await likyly.recommendations.get(...)`. Use `with Likyly(...) as likyly:` (or call `close()`) to release the connection pool.
|
|
278
|
+
- `import` is a Python keyword, so the batch import is `items.import_(...)` / `users.import_(...)`. Fields are `snake_case` (`item_id`, `recommendation_id`); the keys inside `properties` are yours and are never renamed.
|
|
279
|
+
|
|
280
|
+
## Documentation and support
|
|
281
|
+
|
|
282
|
+
- Documentation: <https://likyly.com/docs/sdk/python>
|
|
283
|
+
- REST API reference (OpenAPI): <https://api.likyly.com/docs>
|
|
284
|
+
- Package: likyly on PyPI
|
|
285
|
+
|
|
286
|
+
MIT licensed.
|