pax_global_header00006660000000000000000000000064145023331150014507gustar00rootroot0000000000000052 comment=febd6856953f44c9ecffff6287a2a77c44be24d7 ljmerza-py-our-groceries-febd685/000077500000000000000000000000001450233311500170715ustar00rootroot00000000000000ljmerza-py-our-groceries-febd685/.gitignore000066400000000000000000000000671450233311500210640ustar00rootroot00000000000000build/ dist/ ourgroceries.egg-info/ *.pyc __pycache__/ljmerza-py-our-groceries-febd685/LICENSE000066400000000000000000000020611450233311500200750ustar00rootroot00000000000000Copyright (c) 2018 The Python Packaging Authority Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.ljmerza-py-our-groceries-febd685/README.md000066400000000000000000000046001450233311500203500ustar00rootroot00000000000000Unofficial asyncio python wrapper for the Our Groceries API. This library requires `Python >=3.5`. ## Installation ```bash pip install ourgroceries ``` ## Usage ``` import asyncio from ourgroceries import OurGroceries username = '' password = '' og = OurGroceries(username, password) loop = asyncio.get_event_loop() loop.run_until_complete(og.login()) my_lists = loop.run_until_complete(og.get_my_lists()) print(my_lists) my_todo_list = loop.run_until_complete(og.get_list_items(list_id='')) print(my_todo_list) ``` ## Methods ```def login()``` Logs into our groceries --- ```def get_my_lists()``` Gets all of your lists --- ```def get_category_items()``` Gets all of your category items --- ```def get_list_items(list_id)``` Gets the items for a list --- ```def create_list(name, list_type='SHOPPING')``` Creates a new list. list_type can be 'RECIPES' or 'SHOPPING' --- ```def create_category(name)``` Create a new category --- ```def toggle_item_crossed_off(list_id, item_id, cross_off=False)``` Toggle a list item's crossed off property based on `cross_off` --- ```def add_item_to_list(list_id, value, category="uncategorized", auto_category=False, note=None)``` Adds a new item to a given list/category. Use `auto_category` instead of `category` to let Our Groceries apply the default category for this item. --- ```async def add_items_to_list(self, list_id, items)``` Adds several items to a given list. Use `items` to pass a sequence of items, each being just a value, or a tuple (value, category, note). --- ```def remove_item_from_list(list_id, item_id)``` Removes an item from a given list --- ```def get_master_list()``` Gets the master list --- ```def get_category_list()``` Gets the category list --- ```def delete_list(list_id)``` Deletes a list --- ```def delete_all_crossed_off_from_list(list_id)``` Deletes all crossed off items from a list --- ```def add_item_to_master_list(value, category_id)``` Adds an item to the master list --- ```def change_item_on_list(list_id, item_id, category_id, value)``` Changes an item on a list --- ## Exceptions throws `InvalidLoginException` if can't login. ## Development prerequisites ``` python3 -m pip install --user --upgrade setuptools wheel python3 -m pip install --user --upgrade twine increment version in `setup.py` delete build folder python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* ``` ljmerza-py-our-groceries-febd685/ourgroceries/000077500000000000000000000000001450233311500216015ustar00rootroot00000000000000ljmerza-py-our-groceries-febd685/ourgroceries/__init__.py000066400000000000000000000257771450233311500237340ustar00rootroot00000000000000#!/usr/bin/env python import re import json import aiohttp import logging from .exceptions import InvalidLoginException _LOGGER = logging.getLogger(__name__) # urls used BASE_URL = 'https://www.ourgroceries.com' SIGN_IN = '{}/sign-in'.format(BASE_URL) YOUR_LISTS = '{}/your-lists/'.format(BASE_URL) # cookies COOKIE_KEY_SESSION = 'ourgroceries-auth' # form fields when logging in FORM_KEY_USERNAME = 'emailAddress' FORM_KEY_PASSWORD = 'password' FORM_KEY_ACTION = 'action' FORM_VALUE_ACTION = 'sign-in' # actions to preform on post api ACTION_GET_LIST = 'getList' ACTION_GET_LISTS = 'getOverview' ACTION_ITEM_CROSSED_OFF = 'setItemCrossedOff' ACTION_ITEM_ADD = 'insertItem' ACTION_ITEM_ADD_ITEMS = 'insertItems' ACTION_ITEM_REMOVE = 'deleteItem' ACTION_ITEM_RENAME = 'changeItemValue' ACTION_LIST_CREATE = 'createList' ACTION_LIST_REMOVE = 'deleteList' ACTION_LIST_RENAME = 'renameList' ACTION_GET_MASTER_LIST = 'getMasterList' ACTION_GET_CATEGORY_LIST = 'getCategoryList' ACTION_ITEM_RENAME = 'changeItemValue' ACTION_ITEM_CHANGE_VALUE = 'changeItemValue' ACTION_LIST_DELETE_ALL_CROSSED_OFF = 'deleteAllCrossedOffItems' REGEX_MASTER_LIST_ID = r'g_masterListUrl = "/your-lists/list/(\S*)"' ATTR_CATEGORY_ID = 'categoryId' ATTR_ITEM_NEW_VALUE = 'newValue' # regex to get team id REGEX_TEAM_ID = r'g_teamId = "(.*)";' REGEX_STATIC_METALIST = r'g_staticMetalist = (\[.*\]);' # post body attributes ATTR_LIST_ID = 'listId' ATTR_LIST_NAME = 'name' ATTR_LIST_TYPE = 'listType' ATTR_ITEM_ID = 'itemId' ATTR_ITEM_CROSSED = 'crossedOff' ATTR_ITEM_VALUE = 'value' ATTR_ITEM_CATEGORY = 'categoryId' ATTR_ITEM_NOTE = 'note' ATTR_ITEMS = 'items' ATTR_COMMAND = 'command' ATTR_TEAM_ID = 'teamId' # properties of returned data PROP_LIST = 'list' PROP_ITEMS = 'items' def add_crossed_off_prop(item): """Adds crossed off prop to any items that don't have it.""" item[ATTR_ITEM_CROSSED] = item.get(ATTR_ITEM_CROSSED, False) return item def list_item_to_payload(item, list_id): """Maps a list item (scalar value or a tuple (value, category, note)) to payload""" if isinstance(item, str): payload = { ATTR_ITEM_VALUE: item } else: item = item + (None, None) payload = { ATTR_ITEM_VALUE: item[0], ATTR_ITEM_CATEGORY: item[1], ATTR_ITEM_NOTE: item[2] } payload[ATTR_LIST_ID] = list_id return payload class OurGroceries(): def __init__(self, username, password): """Set Our Groceries username and password.""" self._username = username self._password = password self._session_key = None self._team_id = None async def login(self): """Logs into Our Groceries.""" await self._get_session_cookie() await self._get_team_id() await self._get_master_list_id() _LOGGER.debug('ourgroceries logged in') async def _get_session_cookie(self): """Gets the session cookie value.""" _LOGGER.debug('ourgroceries _get_session_cookie') form_data = aiohttp.FormData() form_data.add_field(FORM_KEY_USERNAME, self._username) form_data.add_field(FORM_KEY_PASSWORD, self._password) form_data.add_field(FORM_KEY_ACTION, FORM_VALUE_ACTION) async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar()) as session: async with session.post(SIGN_IN, data=form_data): cookies = session.cookie_jar.filter_cookies(BASE_URL) for key, cookie in cookies.items(): if key == COOKIE_KEY_SESSION: self._session_key = cookie.value _LOGGER.debug('ourgroceries found _session_key {}'.format(self._session_key)) if not self._session_key: _LOGGER.error('ourgroceries Could not find cookie session') raise InvalidLoginException('Could not find session cookie') async def _get_team_id(self): """Gets the team id for a user.""" _LOGGER.debug('ourgroceries _get_team_id') cookies = {COOKIE_KEY_SESSION: self._session_key} async with aiohttp.ClientSession(cookies=cookies) as session: async with session.get(YOUR_LISTS) as resp: responseText = await resp.text() self._team_id = re.findall(REGEX_TEAM_ID, responseText)[0] _LOGGER.debug('ourgroceries found team_id {}'.format(self._team_id)) static_metalist = json.loads(re.findall(REGEX_STATIC_METALIST, responseText)[0]) categoryList = [list for list in static_metalist if list['listType'] == 'CATEGORY'][0] self._category_id = categoryList['id'] _LOGGER.debug('ourgroceries found category_id {}'.format(self._category_id)) async def _get_master_list_id(self): """Gets the master list id for a user.""" _LOGGER.debug('ourgroceries _get_master_list_id') cookies = {COOKIE_KEY_SESSION: self._session_key} async with aiohttp.ClientSession(cookies=cookies) as session: async with session.get(YOUR_LISTS) as resp: responseText = await resp.text() self._master_list_id = re.findall(REGEX_MASTER_LIST_ID, responseText)[0] _LOGGER.debug('ourgroceries found master_list_id {}'.format(self._master_list_id)) async def get_my_lists(self): """Get our grocery lists.""" _LOGGER.debug('ourgroceries get_my_lists') return await self._post(ACTION_GET_LISTS) async def get_category_items(self): """Get category items.""" _LOGGER.debug('ourgroceries get_category_items') other_payload = {ATTR_LIST_ID: self._category_id} data = await self._post(ACTION_GET_LIST, other_payload) return(data) async def get_list_items(self, list_id): """Get an our grocery list's items.""" _LOGGER.debug('ourgroceries get_list_items') other_payload = {ATTR_LIST_ID: list_id} data = await self._post(ACTION_GET_LIST, other_payload) data[PROP_LIST][PROP_ITEMS] = list(map(add_crossed_off_prop, data[PROP_LIST][PROP_ITEMS])) return data async def create_list(self, name, list_type='SHOPPING'): """Create a new shopping list.""" _LOGGER.debug('ourgroceries create_list') other_payload = { ATTR_LIST_NAME: name, ATTR_LIST_TYPE: list_type.upper(), } return await self._post(ACTION_LIST_CREATE, other_payload) async def create_category(self, name): """Create a new category.""" _LOGGER.debug('ourgroceries create_category') other_payload = { ATTR_ITEM_VALUE: name, ATTR_LIST_ID: self._category_id, } return await self._post(ACTION_ITEM_ADD, other_payload) async def toggle_item_crossed_off(self, list_id, item_id, cross_off=False): """Toggles a lists's item's crossed off property.""" _LOGGER.debug('ourgroceries toggle_item_crossed_off') other_payload = { ATTR_LIST_ID: list_id, ATTR_ITEM_ID: item_id, ATTR_ITEM_CROSSED: cross_off, } return await self._post(ACTION_ITEM_CROSSED_OFF, other_payload) async def add_item_to_list(self, list_id, value, category="uncategorized", auto_category=False, note=None): """Add a new item to a list.""" _LOGGER.debug('ourgroceries add_item_to_list') other_payload = { ATTR_LIST_ID: list_id, ATTR_ITEM_VALUE: value, ATTR_ITEM_CATEGORY: category, ATTR_ITEM_NOTE: note } if auto_category: other_payload.pop(ATTR_ITEM_CATEGORY, None) return await self._post(ACTION_ITEM_ADD, other_payload) async def add_items_to_list(self, list_id, items): """ Add many items to a list. :param items sequence of items, each being just a value, or a tuple (value, category, note). """ _LOGGER.debug('ourgroceries add_items_to_list') other_payload = { ATTR_ITEMS: [list_item_to_payload(i, list_id) for i in items] } return await self._post(ACTION_ITEM_ADD_ITEMS, other_payload) async def remove_item_from_list(self, list_id, item_id): """Remove an item from a list.""" _LOGGER.debug('ourgroceries remove_item_from_list') other_payload = { ATTR_LIST_ID: list_id, ATTR_ITEM_ID: item_id, } return await self._post(ACTION_ITEM_REMOVE, other_payload) async def get_master_list(self): """Get an our grocery list's items.""" _LOGGER.debug('ourgroceries get_list_items') other_payload = {ATTR_LIST_ID: self._master_list_id} return await self._post(ACTION_GET_LIST, other_payload) async def get_category_list(self): """Get our grocery lists.""" _LOGGER.debug('ourgroceries get_master_list') other_payload = {ATTR_TEAM_ID: self._team_id} return await self._post(ACTION_GET_CATEGORY_LIST, other_payload) async def delete_list(self, list_id): """Create a new shopping list.""" _LOGGER.debug('ourgroceries create_list') other_payload = { ATTR_LIST_ID: list_id, ATTR_TEAM_ID: self._team_id, } return await self._post(ACTION_LIST_REMOVE, other_payload) async def delete_all_crossed_off_from_list(self, list_id): """delete all crossed off itemsn from a list.""" _LOGGER.debug('ourgroceries remove_item_from_list') other_payload = { ATTR_LIST_ID: list_id, } return await self._post(ACTION_LIST_DELETE_ALL_CROSSED_OFF, other_payload) async def add_item_to_master_list(self, value, category_id): """Add a new item to a list.""" _LOGGER.debug('ourgroceries add_item_to_list') other_payload = { ATTR_LIST_ID: self._master_list_id, ATTR_ITEM_VALUE: value, ATTR_CATEGORY_ID: category_id, } return await self._post(ACTION_ITEM_ADD, other_payload) async def change_item_on_list(self, list_id, item_id, category_id, value): """Add a new item to a list.""" _LOGGER.debug('ourgroceries add_item_to_list') other_payload = { ATTR_ITEM_ID: item_id, ATTR_LIST_ID: list_id, ATTR_ITEM_NEW_VALUE: value, ATTR_CATEGORY_ID: category_id, ATTR_TEAM_ID: self._team_id, } return await self._post(ACTION_ITEM_CHANGE_VALUE, other_payload) async def _post(self, command, other_payload=None): """Post a command to the API.""" if not self._session_key: await self.login() cookies = {COOKIE_KEY_SESSION: self._session_key} payload = {ATTR_COMMAND: command} if self._team_id: payload[ATTR_TEAM_ID] = self._team_id if other_payload: payload = {**payload, **other_payload} async with aiohttp.ClientSession(cookies=cookies) as session: async with session.post(YOUR_LISTS, json=payload) as resp: return await resp.json() ljmerza-py-our-groceries-febd685/ourgroceries/exceptions.py000066400000000000000000000000621450233311500243320ustar00rootroot00000000000000 class InvalidLoginException(Exception): passljmerza-py-our-groceries-febd685/setup.py000066400000000000000000000015421450233311500206050ustar00rootroot00000000000000from setuptools import setup, find_packages PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ 'aiohttp>=3.6.1', 'beautifulsoup4>=4.7.1' ] setup( name='ourgroceries', version='1.5.3', author="Leonardo Merza", author_email="ljmerza@gmail.com", keywords='unoffical our groceries api', description="Our Groceries Unofficial Python Package", long_description=open('README.md').read(), long_description_content_type="text/markdown", url="https://github.com/ljmerza/py-our-groceries", license='MIT', packages=PACKAGES, include_package_data=True, python_requires='>=3.5', zip_safe=False, install_requires=REQUIRES, classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], ) ljmerza-py-our-groceries-febd685/tests/000077500000000000000000000000001450233311500202335ustar00rootroot00000000000000ljmerza-py-our-groceries-febd685/tests/test_api.py000066400000000000000000000004341450233311500224160ustar00rootroot00000000000000 import asyncio from ourgroceries import OurGroceries username = '' password = '' og = OurGroceries(username, password) asyncio.run(og.login()) my_lists = asyncio.run(og.get_my_lists()) print(my_lists) my_todo_list = asyncio.run(og.get_list_items(list_id='')) print(my_todo_list)