diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2a285c --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# cellartracker + +Python package to export data from [CellarTracker](https://www.cellartracker.com), the wine cellar management platform. + +[![PyPI](https://img.shields.io/pypi/v/cellartracker.svg)](https://pypi.python.org/pypi/cellartracker) +[![License](https://img.shields.io/pypi/l/cellartracker.svg)](https://github.com/mathroule/cellartracker/blob/main/LICENSE) + +## Install + +```bash +pip install cellartracker +``` + +## Quick Start + +```python +from cellartracker import cellartracker + +ct = cellartracker.CellarTracker('your_handle', 'your_password') + +# Get your full inventory +inventory = ct.get_inventory() +print(f"Your cellar: {len(inventory)} bottles") + +# First bottle +bottle = inventory[0] +print(f"{bottle['Vintage']} {bottle['Wine']} — ${bottle['Price']}") +``` + +## Available Methods + +### Inventory & Wines +| Method | Returns | Description | +|--------|---------|-------------| +| `get_inventory()` | `list[dict]` | Full inventory with valuations, scores, purchase details | +| `get_list()` | `list[dict]` | Current wine list with stock levels | +| `get_bottles()` | `list[dict]` | Every bottle individually (for state tracking) | +| `get_availability()` | `list[dict]` | Drinkability report with drinking windows | + +### Purchase & Consumption +| Method | Returns | Description | +|--------|---------|-------------| +| `get_purchase()` | `list[dict]` | Purchase history with store, price, quantity | +| `get_consumed()` | `list[dict]` | Consumption history | +| `get_pending()` | `list[dict]` | Pending deliveries | + +### Reviews & Tags +| Method | Returns | Description | +|--------|---------|-------------| +| `get_notes()` | `list[dict]` | Public tasting notes and ratings | +| `get_private_notes()` | `list[dict]` | Your private tasting notes | +| `get_pro_review()` | `list[dict]` | Professional critic scores | +| `get_tag()` | `list[dict]` | Wish list / tagged wines | +| `get_food_tag()` | `list[dict]` | Food pairing tags | + +## Common Fields + +Each bottle in `get_inventory()` includes these useful fields: + +| Field | Description | +|-------|-------------| +| `iWine` | Unique wine ID (for API operations) | +| `Wine` | Wine name | +| `Vintage` | Vintage year | +| `Producer` | Producer / winery | +| `Varietal` | Grape variety | +| `Country` / `Region` | Origin | +| `Price` | Purchase price | +| `Valuation` | Current market valuation | +| `StoreName` | Where purchased | +| `PurchaseDate` | Date of purchase | +| `Location` | Storage location | +| `Size` | Bottle size (750ml, 1.5L, etc.) | +| `CT` | Community score | +| `CNotes` | Number of community tasting notes | + +## CLI Usage + +```bash +# Export your inventory as tab-separated data +cellartracker --username=your_handle --password=your_password --table=Inventory + +# Export purchase history as CSV +cellartracker --username=your_handle --password=your_password --table=Purchase --format=csv +``` + +Available tables: `List`, `Inventory`, `Notes`, `PrivateNotes`, `Purchase`, `Pending`, `Consumed`, `Availability`, `Tag`, `ProReview`, `Bottles`, `FoodTag` + +Available formats: `tab`, `csv`, `xml`, `html` + +## Authentication + +Your CellarTracker **handle** is required — this is your username, not your email address. If you sign in at cellartracker.com as "coop789", that's your handle. + +## Data Source + +This package uses CellarTracker's [official export functionality](https://support.cellartracker.com/article/29-exporting-data) to access your data via the `xlquery.asp` endpoint. + +## License + +MIT license. diff --git a/cellartracker/cellartracker.py b/cellartracker/cellartracker.py index 5481e47..e2c4ee5 100755 --- a/cellartracker/cellartracker.py +++ b/cellartracker/cellartracker.py @@ -1,8 +1,13 @@ -"""Main module.""" +"""Main module for CellarTracker read API. + +Provides the CellarTracker class for exporting data from CellarTracker +via the xlquery.asp export endpoint. +""" + import csv import logging - from io import StringIO +from typing import Any, Optional from .client import CellarTrackerClient from .enum import CellarTrackerFormat, CellarTrackerTable @@ -11,76 +16,145 @@ class CellarTracker(object): - """ - CellarTracker is the class handling the CellarTracker data export. + """CellarTracker data export client. + + Provides read-only access to CellarTracker data via the official + export endpoint. Each method returns a list of dicts with the + fields available for that table. + + Args: + username: CellarTracker handle (not email) + password: CellarTracker account password + + Example: + >>> from cellartracker import cellartracker + >>> ct = cellartracker.CellarTracker('myhandle', 'mypassword') + >>> inventory = ct.get_inventory() + >>> len(inventory) + 82 + >>> inventory[0]['Wine'] + 'Château Lafite Rothschild' """ - def __init__(self, username: None, password: None): + def __init__(self, username: str, password: str): self.client = CellarTrackerClient(username, password) - def get_list(self): - """Get list.""" + def get_list(self) -> list[dict[str, Any]]: + """Get your current wine list with stock levels. + + Returns a list of wines currently in your cellar with + quantity, location, and size information. + + Keys include: iWine, Wine, Vintage, Location, Quantity, Size + """ return self._get_data(table=CellarTrackerTable.List) - def get_inventory(self): - """Get inventory.""" + def get_inventory(self) -> list[dict[str, Any]]: + """Get full inventory with valuations and purchase details. + + The most comprehensive table. Returns all bottles with + pricing, valuation, store, purchase date, tasting scores, + and community stats. + + Keys include: iWine, Wine, Vintage, Location, Price, Valuation, + StoreName, PurchaseDate, Size, Varietal, Producer, Country, + CT (community score), and many more. + """ return self._get_data(table=CellarTrackerTable.Inventory) - def get_notes(self): - """Get notes.""" + def get_notes(self) -> list[dict[str, Any]]: + """Get public tasting notes. + + Returns community tasting notes with ratings. + Keys include: iNote, iWine, Vintage, Wine, Note, Rating, Date + """ return self._get_data(table=CellarTrackerTable.Notes) - def get_private_notes(self): - """Get private notes data.""" + def get_private_notes(self) -> list[dict[str, Any]]: + """Get private tasting notes (your notes only).""" return self._get_data(table=CellarTrackerTable.PrivateNotes) - def get_purchase(self): - """Get purchase data.""" + def get_purchase(self) -> list[dict[str, Any]]: + """Get purchase history. + + Returns all purchase records with store, price, quantity, + and delivery status. + + Keys include: iWine, iPurchase, PurchaseDate, StoreName, + Price, Quantity, Remaining, Size, Wine, Vintage + """ return self._get_data(table=CellarTrackerTable.Purchase) - def get_pending(self): - """Get pending.""" + def get_pending(self) -> list[dict[str, Any]]: + """Get pending deliveries (ordered but not yet received).""" return self._get_data(table=CellarTrackerTable.Pending) - def get_consumed(self): - """Get consumed.""" + def get_consumed(self) -> list[dict[str, Any]]: + """Get consumption history (bottles marked as consumed/drunk). + + Keys include: iConsumed, iWine, Vintage, Wine, Consumed, Type + """ return self._get_data(table=CellarTrackerTable.Consumed) - def get_availability(self): - """Get availability.""" + def get_availability(self) -> list[dict[str, Any]]: + """Get drinkability report with drinking windows. + + Keys include: iWine, Wine, Vintage, Available, BeginConsume, + EndConsume, Type, Color, Varietal + """ return self._get_data(table=CellarTrackerTable.Availability) - def get_tag(self): - """Get tag.""" + def get_tag(self) -> list[dict[str, Any]]: + """Get wish list / tagged wines.""" return self._get_data(table=CellarTrackerTable.Tag) - def get_pro_review(self): - """Get pro review.""" + def get_pro_review(self) -> list[dict[str, Any]]: + """Get professional critic reviews (WA, WS, etc.).""" return self._get_data(table=CellarTrackerTable.ProReview) - def get_bottles(self): - """Get bottles.""" + def get_bottles(self) -> list[dict[str, Any]]: + """Get individual bottle records with state tracking. + + Returns every bottle individually (vs aggregated by wine). + Keys include: BottleState, Barcode, iWine, Vintage, Wine, + Location, Size + + Useful for tracking individual bottle movement and states. + """ return self._get_data(table=CellarTrackerTable.Bottles) - def get_food_tag(self): - """Get food tag.""" + def get_food_tag(self) -> list[dict[str, Any]]: + """Get food pairing tags.""" return self._get_data(table=CellarTrackerTable.FoodTag) - def _get_data(self, table: CellarTrackerTable): - """Get data.""" + def _get_data(self, table: CellarTrackerTable) -> list[dict[str, Any]]: + """Fetch and parse tab-separated data from CellarTracker. + + Args: + table: The table to export + + Returns: + List of dicts with column names as keys + """ return _parse_data( self.client.get(table=table, format=CellarTrackerFormat.tab) ) -def _parse_data(data: str): +def _parse_data(data: str) -> list[dict[str, str]]: + """Parse tab-separated data from CellarTracker. + + Args: + data: Raw tab-separated string from the export API + + Returns: + List of dicts with header columns as keys + """ reader = csv.DictReader(StringIO(data), dialect="excel-tab") results = [] for row in reader: result = {} for key, value in row.items(): result[key] = value - results.append(result) - return results diff --git a/cellartracker/cli.py b/cellartracker/cli.py index 7dd2ff6..e31f574 100644 --- a/cellartracker/cli.py +++ b/cellartracker/cli.py @@ -1,43 +1,104 @@ -"""Console script for CellarTracker""" +"""Console script for CellarTracker.""" + import argparse import sys +import json from .cellartracker import CellarTracker from .enum import CellarTrackerFormat, CellarTrackerTable +TABLE_DESCRIPTIONS = { + "Inventory": "Full inventory with valuations and scores", + "List": "Current wine list with stock levels", + "Purchase": "Purchase history", + "Consumed": "Consumption history", + "Bottles": "Individual bottle records", + "Notes": "Public tasting notes", + "PrivateNotes": "Private tasting notes", + "Pending": "Pending deliveries", + "Availability": "Drinkability / drinking windows", + "Tag": "Wish list / tagged wines", + "ProReview": "Professional critic reviews", + "FoodTag": "Food pairing tags", +} + + def main(): - """Console script for CellarTracker""" - parser = argparse.ArgumentParser() - parser.add_argument('-u', '--username', - required=True, - help='Username from CellarTracker') - parser.add_argument('-p', '--password', - required=True, - help='Password from CellarTracker') - parser.add_argument('-t', '--table', - required=False, - help='Table from CellarTracker', - choices=CellarTrackerTable.__members__, - default=CellarTrackerTable.List.value) - parser.add_argument('-f', '--format', - required=False, - help='Format from CellarTracker', - choices=CellarTrackerFormat.__members__, - default=CellarTrackerFormat.tab.value) + """Console script for CellarTracker.""" + parser = argparse.ArgumentParser( + description="Export your CellarTracker wine data.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=( + "Examples:\n" + " cellartracker -u myhandle -p mypass --table=Inventory\n" + " cellartracker -u myhandle -p mypass --table=Purchase --format=csv\n" + " cellartracker -u myhandle -p mypass --table=Inventory --json\n" + ), + ) + parser.add_argument( + "-u", "--username", + required=True, + help="CellarTracker handle (your username, not email)", + ) + parser.add_argument( + "-p", "--password", + required=True, + help="CellarTracker account password", + ) + parser.add_argument( + "-t", "--table", + required=False, + help="Data table to export", + choices=sorted(CellarTrackerTable.__members__.keys()), + default="List", + ) + parser.add_argument( + "-f", "--format", + required=False, + help="Output format from the API", + choices=sorted(CellarTrackerFormat.__members__.keys()), + default="tab", + ) + parser.add_argument( + "--json", + required=False, + action="store_true", + help="Output as formatted JSON", + ) + parser.add_argument( + "--list-tables", + required=False, + action="store_true", + help="List available tables and exit", + ) + args = parser.parse_args() + if args.list_tables: + print("Available tables:") + for name, desc in sorted(TABLE_DESCRIPTIONS.items()): + print(f" {name:15s} {desc}") + return 0 + try: - cellartracker = CellarTracker(args.username, args.password) - response = cellartracker.client.get( - table=CellarTrackerTable[args.table], - format=CellarTrackerFormat[args.format]) - print(response) + ct = CellarTracker(args.username, args.password) + table_enum = CellarTrackerTable[args.table] + format_enum = CellarTrackerFormat[args.format] + + response = ct.client.get(table=table_enum, format=format_enum) + + if args.json: + # Parse tab/CSV data back to structured format + data = ct._get_data(table=table_enum) + print(json.dumps(data, indent=2, default=str)) + else: + print(response) return 0 except BaseException as exp: - print(exp) + print(f"Error: {exp}", file=sys.stderr) return 1 if __name__ == "__main__": - sys.exit(main()) # pragma: no cover + sys.exit(main()) diff --git a/cellartracker/client.py b/cellartracker/client.py index 9a7a751..72e4e38 100755 --- a/cellartracker/client.py +++ b/cellartracker/client.py @@ -4,8 +4,13 @@ class CellarTrackerClient(object): - def __init__(self, username: None, password: None): - """Initialize the client object.""" + def __init__(self, username: str, password: str): + """Initialize the client object. + + Args: + username: CellarTracker handle + password: CellarTracker account password + """ self._api = CellarTrackerAPI() self._username = username self._password = password diff --git a/setup.py b/setup.py index 6a6f63c..e39b8e5 100644 --- a/setup.py +++ b/setup.py @@ -4,13 +4,16 @@ from setuptools import setup, find_packages -with open('README.rst') as readme_file: - readme = readme_file.read() +with open('README.rst') as readme_rst_file: + readme = readme_rst_file.read() + +with open('README.md') as readme_md_file: + readme_md = readme_md_file.read() with open('HISTORY.rst') as history_file: history = history_file.read() -requirements = ['requests'], +requirements = ['requests'] setup_requirements = [ ]