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.
@@ -0,0 +1,5 @@
1
+ /.venv/
2
+ /dist/
3
+ *.egg-info/
4
+ __pycache__/
5
+ *.pyc
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.