pyanylist 0.0.2__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
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.
pyanylist/__init__.py
ADDED
|
Binary file
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyanylist
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Rust
|
|
11
|
+
Requires-Dist: maturin>=1.0 ; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
|
|
14
|
+
Requires-Dist: ruff>=0.8.0 ; extra == 'dev'
|
|
15
|
+
Requires-Dist: pyright>=1.1.0 ; extra == 'dev'
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Summary: Python bindings for AnyList API via anylist_rs
|
|
19
|
+
Keywords: anylist,shopping,groceries,api,rust
|
|
20
|
+
License: MIT
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
23
|
+
Project-URL: Homepage, https://github.com/ozonejunkieau/pyanylist
|
|
24
|
+
Project-URL: Issues, https://github.com/ozonejunkieau/pyanylist/issues
|
|
25
|
+
Project-URL: Repository, https://github.com/ozonejunkieau/pyanylist
|
|
26
|
+
|
|
27
|
+
# pyanylist
|
|
28
|
+
|
|
29
|
+
[](https://github.com/ozonejunkieau/pyanylist/actions/workflows/ci.yml)
|
|
30
|
+
[](https://pypi.org/project/pyanylist/)
|
|
31
|
+
[](https://pypi.org/project/pyanylist/)
|
|
32
|
+
|
|
33
|
+
Unofficial Python bindings for the [AnyList](https://www.anylist.com/) API, built with Rust and PyO3 for performance.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Shopping Lists**: Create, read, update, and delete shopping lists and items
|
|
38
|
+
- **Favourites**: Manage favourite/starter items
|
|
39
|
+
- **Recipes**: Full recipe CRUD with ingredient scaling
|
|
40
|
+
- **Meal Planning**: iCalendar integration for meal plan calendars
|
|
41
|
+
- **Real-time Sync**: WebSocket-based live updates
|
|
42
|
+
- **Token Persistence**: Save and restore authentication sessions
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv add pyanylist
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or with pip:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install pyanylist
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from pyanylist import AnyListClient
|
|
60
|
+
|
|
61
|
+
# Login with email and password
|
|
62
|
+
client = AnyListClient.login("your-email@example.com", "your-password")
|
|
63
|
+
|
|
64
|
+
# Get all shopping lists
|
|
65
|
+
lists = client.get_lists()
|
|
66
|
+
for lst in lists:
|
|
67
|
+
print(f"{lst.name}: {len(lst.items)} items")
|
|
68
|
+
|
|
69
|
+
# Add an item to a list
|
|
70
|
+
item = client.add_item(lists[0].id, "Milk")
|
|
71
|
+
print(f"Added: {item.name}")
|
|
72
|
+
|
|
73
|
+
# Add item with details
|
|
74
|
+
item = client.add_item_with_details(
|
|
75
|
+
lists[0].id,
|
|
76
|
+
"Apples",
|
|
77
|
+
quantity="6",
|
|
78
|
+
details="Honeycrisp preferred",
|
|
79
|
+
category="Produce"
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Authentication
|
|
84
|
+
|
|
85
|
+
### Login with credentials
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
client = AnyListClient.login("email@example.com", "password")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Save and restore session
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# Export tokens for persistence
|
|
95
|
+
tokens = client.export_tokens()
|
|
96
|
+
|
|
97
|
+
# Save to file, database, etc.
|
|
98
|
+
save_tokens(tokens.access_token, tokens.refresh_token, tokens.user_id)
|
|
99
|
+
|
|
100
|
+
# Later, restore from saved tokens
|
|
101
|
+
from pyanylist import SavedTokens
|
|
102
|
+
|
|
103
|
+
tokens = SavedTokens(
|
|
104
|
+
access_token="...",
|
|
105
|
+
refresh_token="...",
|
|
106
|
+
user_id="...",
|
|
107
|
+
is_premium_user=False
|
|
108
|
+
)
|
|
109
|
+
client = AnyListClient.from_tokens(tokens)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Shopping Lists
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# Get all lists
|
|
116
|
+
lists = client.get_lists()
|
|
117
|
+
|
|
118
|
+
# Get a specific list
|
|
119
|
+
grocery_list = client.get_list_by_name("Groceries")
|
|
120
|
+
# or by ID
|
|
121
|
+
grocery_list = client.get_list_by_id("list-id-here")
|
|
122
|
+
|
|
123
|
+
# Create a new list
|
|
124
|
+
new_list = client.create_list("Weekly Shopping")
|
|
125
|
+
|
|
126
|
+
# Rename a list
|
|
127
|
+
client.rename_list(new_list.id, "Monthly Shopping")
|
|
128
|
+
|
|
129
|
+
# Delete a list
|
|
130
|
+
client.delete_list(new_list.id)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## List Items
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# Add a simple item
|
|
137
|
+
item = client.add_item(list_id, "Bread")
|
|
138
|
+
|
|
139
|
+
# Add item with details
|
|
140
|
+
item = client.add_item_with_details(
|
|
141
|
+
list_id,
|
|
142
|
+
"Chicken Breast",
|
|
143
|
+
quantity="2 lbs",
|
|
144
|
+
details="Boneless, skinless",
|
|
145
|
+
category="Meat"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Check/uncheck items
|
|
149
|
+
client.cross_off_item(list_id, item.id)
|
|
150
|
+
client.uncheck_item(list_id, item.id)
|
|
151
|
+
|
|
152
|
+
# Delete an item
|
|
153
|
+
client.delete_item(list_id, item.id)
|
|
154
|
+
|
|
155
|
+
# Clear all checked items
|
|
156
|
+
client.delete_all_crossed_off_items(list_id)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Favourites
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
# Get all favourite items
|
|
163
|
+
favourites = client.get_favourites()
|
|
164
|
+
|
|
165
|
+
# Get favourites organized by list
|
|
166
|
+
fav_lists = client.get_favourites_lists()
|
|
167
|
+
for fav_list in fav_lists:
|
|
168
|
+
print(f"{fav_list.name}: {len(fav_list.items)} items")
|
|
169
|
+
|
|
170
|
+
# Add a favourite
|
|
171
|
+
fav = client.add_favourite("Coffee", category="Beverages")
|
|
172
|
+
|
|
173
|
+
# Remove a favourite
|
|
174
|
+
client.remove_favourite(fav_list.id, fav.id)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Recipes
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from pyanylist import Ingredient
|
|
181
|
+
|
|
182
|
+
# Get all recipes
|
|
183
|
+
recipes = client.get_recipes()
|
|
184
|
+
for recipe in recipes:
|
|
185
|
+
print(f"{recipe.name}: {len(recipe.ingredients)} ingredients")
|
|
186
|
+
|
|
187
|
+
# Get a specific recipe
|
|
188
|
+
recipe = client.get_recipe_by_name("Pasta Carbonara")
|
|
189
|
+
|
|
190
|
+
# Create a recipe
|
|
191
|
+
ingredients = [
|
|
192
|
+
Ingredient("Spaghetti", quantity="400g"),
|
|
193
|
+
Ingredient("Eggs", quantity="4"),
|
|
194
|
+
Ingredient("Parmesan", quantity="100g", note="freshly grated"),
|
|
195
|
+
Ingredient("Pancetta", quantity="200g"),
|
|
196
|
+
]
|
|
197
|
+
steps = [
|
|
198
|
+
"Boil pasta according to package directions",
|
|
199
|
+
"Fry pancetta until crispy",
|
|
200
|
+
"Mix eggs and parmesan",
|
|
201
|
+
"Combine everything off heat",
|
|
202
|
+
]
|
|
203
|
+
recipe = client.create_recipe("Carbonara", ingredients, steps)
|
|
204
|
+
|
|
205
|
+
# Add recipe ingredients to a shopping list
|
|
206
|
+
client.add_recipe_to_list(recipe.id, list_id)
|
|
207
|
+
|
|
208
|
+
# Scale recipe (e.g., double it)
|
|
209
|
+
client.add_recipe_to_list(recipe.id, list_id, scale_factor=2.0)
|
|
210
|
+
|
|
211
|
+
# Delete a recipe
|
|
212
|
+
client.delete_recipe(recipe.id)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Meal Plan Calendar (iCalendar)
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
# Enable iCalendar export
|
|
219
|
+
info = client.enable_icalendar()
|
|
220
|
+
print(f"Calendar URL: {info.url}")
|
|
221
|
+
# Use this URL in any calendar app (Google Calendar, Apple Calendar, etc.)
|
|
222
|
+
|
|
223
|
+
# Get existing calendar URL
|
|
224
|
+
url = client.get_icalendar_url()
|
|
225
|
+
|
|
226
|
+
# Disable iCalendar
|
|
227
|
+
client.disable_icalendar()
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Real-time Sync
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
import time
|
|
234
|
+
from pyanylist import SyncEvent
|
|
235
|
+
|
|
236
|
+
# Start real-time sync
|
|
237
|
+
sync = client.start_realtime_sync()
|
|
238
|
+
|
|
239
|
+
# Poll for events
|
|
240
|
+
while sync.is_connected():
|
|
241
|
+
events = sync.poll_events()
|
|
242
|
+
for event in events:
|
|
243
|
+
if event == SyncEvent.ShoppingListsChanged:
|
|
244
|
+
print("Lists changed! Refreshing...")
|
|
245
|
+
lists = client.get_lists()
|
|
246
|
+
elif event == SyncEvent.RecipeDataChanged:
|
|
247
|
+
print("Recipes changed!")
|
|
248
|
+
time.sleep(1)
|
|
249
|
+
|
|
250
|
+
# Disconnect when done
|
|
251
|
+
sync.disconnect()
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Available Sync Events
|
|
255
|
+
|
|
256
|
+
- `ShoppingListsChanged` - Shopping lists modified
|
|
257
|
+
- `StarterListsChanged` - Favourites modified
|
|
258
|
+
- `RecipeDataChanged` - Recipes modified
|
|
259
|
+
- `MealPlanCalendarChanged` - Meal plan modified
|
|
260
|
+
- `AccountDeleted` - Account was deleted
|
|
261
|
+
|
|
262
|
+
## Error Handling
|
|
263
|
+
|
|
264
|
+
All methods raise `RuntimeError` on failure:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
try:
|
|
268
|
+
client = AnyListClient.login("bad@email.com", "wrongpassword")
|
|
269
|
+
except RuntimeError as e:
|
|
270
|
+
print(f"Login failed: {e}")
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
client.get_list_by_id("nonexistent-id")
|
|
274
|
+
except RuntimeError as e:
|
|
275
|
+
print(f"List not found: {e}")
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Development
|
|
279
|
+
|
|
280
|
+
### Prerequisites
|
|
281
|
+
|
|
282
|
+
- Python 3.12+
|
|
283
|
+
- Rust 1.70+
|
|
284
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
285
|
+
- protoc (Protocol Buffers compiler)
|
|
286
|
+
- Ubuntu/Debian: `sudo apt-get install protobuf-compiler`
|
|
287
|
+
- macOS: `brew install protobuf`
|
|
288
|
+
|
|
289
|
+
### Setup
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# Clone the repository
|
|
293
|
+
git clone https://github.com/ozonejunkieau/pyanylist.git
|
|
294
|
+
cd pyanylist
|
|
295
|
+
|
|
296
|
+
# Install dependencies (includes dev dependencies)
|
|
297
|
+
uv sync
|
|
298
|
+
|
|
299
|
+
# Build and install in development mode
|
|
300
|
+
uv run maturin develop
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Running Tests
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
# Run unit tests (no credentials needed)
|
|
307
|
+
uv run pytest -v -m "not integration"
|
|
308
|
+
|
|
309
|
+
# Run all tests including integration (requires credentials)
|
|
310
|
+
export ANYLIST_EMAIL="your-email@example.com"
|
|
311
|
+
export ANYLIST_PASSWORD="your-password"
|
|
312
|
+
uv run pytest -v
|
|
313
|
+
|
|
314
|
+
# Run with coverage
|
|
315
|
+
uv run pytest --cov=tests --cov-report=term-missing
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Linting & Type Checking
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# Check code style
|
|
322
|
+
uv run ruff check tests/
|
|
323
|
+
|
|
324
|
+
# Format code
|
|
325
|
+
uv run ruff format tests/
|
|
326
|
+
|
|
327
|
+
# Type check
|
|
328
|
+
uv run pyright tests/
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Acknowledgements
|
|
332
|
+
|
|
333
|
+
This library is built on top of [anylist_rs](https://github.com/phildenhoff/anylist_rs) by Phil Denhoff, which provides the core Rust implementation for interacting with the AnyList API.
|
|
334
|
+
|
|
335
|
+
## License
|
|
336
|
+
|
|
337
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
338
|
+
|
|
339
|
+
## Disclaimer
|
|
340
|
+
|
|
341
|
+
This is an unofficial library and is not affiliated with or endorsed by AnyList. Use at your own risk and in accordance with AnyList's terms of service.
|
|
342
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pyanylist/__init__.py,sha256=49VoTTxmdDGe0jXvUeZOpJav6uUbjEw0j12bKP2Isio,119
|
|
2
|
+
pyanylist/pyanylist.cpython-313-x86_64-linux-gnu.so,sha256=g9YfZtVQhFfO5WBTthT1xGxnmBrb3d5sgoV6M5pRpmk,11834136
|
|
3
|
+
pyanylist-0.0.2.dist-info/METADATA,sha256=_wdxtwJyZI-iLxxOo2sIzh3_1aKpdM-Y_WN8WFBpFyQ,8304
|
|
4
|
+
pyanylist-0.0.2.dist-info/WHEEL,sha256=Gz4wejxs9eM81kW9BZvADtVE2A8dclXu3RnAd46zegY,147
|
|
5
|
+
pyanylist-0.0.2.dist-info/licenses/LICENSE,sha256=UZ2oHB44pk3wlqyZDbAKFQBrHE4UaM1Q7t_QMMTDMLo,1070
|
|
6
|
+
pyanylist-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ozonejunkieau
|
|
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.
|