diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6e5e523..862f5ac 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.17.0 +current_version = 2.1.2 commit = True tag = True tag_name = {new_version} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e501930..43fffb3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,13 +12,13 @@ jobs: - name: Set up Python uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: "3.13" - name: Install Python dependencies - run: pip install black==20.8b1 flake8==3.8.3 + run: pip install black==22.6.0 flake8==5.0.4 - name: Run linters - uses: samuelmeuli/lint-action@v1.5.3 + uses: wearerequired/lint-action with: github_token: ${{ secrets.github_token }} # Enable linters diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e39b3a..6d4121f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v2 @@ -24,6 +24,3 @@ jobs: - name: Test with pytest run: | pytest - env: - MFP_INTEGRATION_TESTING_USERNAME: ${{ secrets.MFP_INTEGRATION_TESTING_USERNAME }} - MFP_INTEGRATION_TESTING_PASSWORD: ${{ secrets.MFP_INTEGRATION_TESTING_PASSWORD }} diff --git a/.gitignore b/.gitignore index c0a6a5f..6f21d4e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ share/* pip-selfcheck.json env/* .vscode/* -.mypy_cache/* \ No newline at end of file +.mypy_cache/* +.idea/ +venv/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53fcafe..0c7a3a5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,31 +4,24 @@ repos: # Sort imports prior to black reformatting, to # ensure black always takes prescedence - repo: https://github.com/timothycrosley/isort - rev: 4.3.21 + rev: 6.0.1 hooks: - id: isort - repo: https://github.com/ambv/black - rev: 20.8b1 + rev: 25.1.0 hooks: - id: black - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + - repo: https://github.com/pycqa/flake8 + rev: 7.1.2 hooks: - - id: end-of-file-fixer - files: '.*\.py$' - id: flake8 - additional_dependencies: - - flake8-bugbear==19.3.0 - - flake8-printf-formatting==1.1.0 - - flake8-pytest==1.3 - - flake8-pytest-style==0.1.3 - repo: https://github.com/asottile/pyupgrade - rev: v1.25.1 + rev: v3.19.1 hooks: - id: pyupgrade - args: ["--py37-plus"] + args: ["--py39-plus"] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.782 + rev: v1.15.0 hooks: - id: mypy args: @@ -36,3 +29,6 @@ repos: - --show-error-codes - --show-error-context - --ignore-missing-imports + additional_dependencies: + - types-requests + - types-python-dateutil diff --git a/changelog.markdown b/changelog.markdown index e51aba6..033488b 100644 --- a/changelog.markdown +++ b/changelog.markdown @@ -1,3 +1,7 @@ +# 2.0.0 (2022-08-26) + +* Now uses the [`browser_cookie3`](https://github.com/borisbabic/browser_cookie3) library for gathering log in credentials instead of logging in to MyFitnessPal directly. This became necessary due to the recent addition of a hidden captcha in the log-in flow for MyFitnessPal; see [Issue #144](https://github.com/coddingtonbear/python-myfitnesspal/issues/144) for details. + # 1.13 (2018-11-20) * Adds support for searching for food items and accessing their nutritional values. Thanks @pydolan! diff --git a/docs/source/cmdline.rst b/docs/source/cmdline.rst index 909b6ed..8311433 100644 --- a/docs/source/cmdline.rst +++ b/docs/source/cmdline.rst @@ -3,23 +3,11 @@ Using the Command-Line API Although most people will probably be using Python-MyFitnessPal as a way of integrating their MyFitnessPal data into another application, -Python-MyFitnessPal does provide a command-line API with a handful of -commands described below. +Python-MyFitnessPal does provide a command-line API with a single +command described below. -``store-password $USERNAME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Store a password for a given MyFitnessPal account in your system’s -keyring. - -``delete-password $USERNAME`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Delete a password for a given MyFitnessPal account from your system -keyring. - -``day $USERNAME [$DATE]`` -~~~~~~~~~~~~~~~~~~~~~~~~~ +``day [$DATE]`` +~~~~~~~~~~~~~~~ Display meals and totals for a given date. If no date is specified, totals will be printed for today. diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index aa38093..0863adf 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -18,16 +18,23 @@ You can either install from pip:: Authentication -------------- -It is a good security practice to not type out the passwords for any of your services (including MyFitnessPal) in either a source file or in the console in such a way that somebody else might be able to read them. Toward that end, python-myfitnesspal allows you to use your system keyring. - -To store your MyFitnessPal password in the system keyring, run:: - - myfitnesspal store-password my_username - -You will immediately be asked for your password, and that password will be stored in your system keyring for later interactions with MyFitnessPal. - -Please note that all examples below *assume* you've stored your password in your system keyring like above, but you can also provide your password by providing your password as a keyword argument to the `myfitnesspal.Client` instance: - -.. code:: python - - client = myfitnesspal.Client('my_username', password='my_password') +This library uses your local browser's MyFitnessPal cookies +for interacting with MyFitnessPal via the `browser_cookie3 `_ library. +To control which user account this library uses for interacting with MyFitnessPal, +just log in to the appropriate account in your browser +and, +with a bit of luck, +python-myfitnesspal should be able to find the authentication credentials needed. + +By default, this library will look for cookies set for the ``www.myfitnesspal.com`` and ``myfitnesspal.com`` domains in all browsers supported by ``browser_cookie3``. You can control which cookiejar is used by passing a ``http.cookiejar.CookieJar`` object via the constructor's `cookiejar` keyword parameter. See `browser_cookie3's readme `_ for details around how you might select a cookiejar from a particular browser. + +.. note:: + + Starting on August 25th, 2022, MyFitnessPal added + a hidden captcha to their login flow. + That change unfortunately prevents this library from logging-in directly, + and as of version 2.0 of python-myfitnesspal, + this library now relies on reading browser cookies directly + for gathering login credentials. + + See `Issue #144 `_ for details and context. diff --git a/docs/source/how_to/diary.rst b/docs/source/how_to/diary.rst index 44cf32e..e8a1d5c 100644 --- a/docs/source/how_to/diary.rst +++ b/docs/source/how_to/diary.rst @@ -7,7 +7,7 @@ To access a single day’s information: import myfitnesspal - client = myfitnesspal.Client('my_username') + client = myfitnesspal.Client() day = client.get_date(2013, 3, 2) day @@ -133,7 +133,7 @@ argument. .. code:: python - client = myfitnesspal.Client('my_username', unit_aware=True) + client = myfitnesspal.Client(unit_aware=True) day = client.get_date(2013, 3, 2) lunch = day['lunch'] print lunch diff --git a/docs/source/how_to/exercises.rst b/docs/source/how_to/exercises.rst index 0ea40ae..e1fa89e 100644 --- a/docs/source/how_to/exercises.rst +++ b/docs/source/how_to/exercises.rst @@ -11,7 +11,7 @@ To get a list of cardiovascular exercises import myfitnesspal - client = myfitnesspal.Client('my_username') + client = myfitnesspal.Client() day = client.get_date(2019, 3, 12) @@ -38,7 +38,7 @@ To get a list of strength exercises import myfitnesspal - client = myfitnesspal.Client('my_username') + client = myfitnesspal.Client() day = client.get_date(2019, 3, 12) diff --git a/docs/source/how_to/food_search.rst b/docs/source/how_to/food_search.rst index 5f463ee..ca09def 100644 --- a/docs/source/how_to/food_search.rst +++ b/docs/source/how_to/food_search.rst @@ -7,7 +7,7 @@ To search for items: import myfitnesspal - client = myfitnesspal.Client('my_username') + client = myfitnesspal.Client() food_items = client.get_food_search_results("bacon cheeseburger") food_items @@ -34,7 +34,7 @@ To get details for a particular food: import myfitnesspal - client = myfitnesspal.Client('my_username') + client = myfitnesspal.Client() item = client.get_food_item_details("89755756637885") item.servings diff --git a/docs/source/how_to/friends_diary.rst b/docs/source/how_to/friends_diary_public.rst similarity index 69% rename from docs/source/how_to/friends_diary.rst rename to docs/source/how_to/friends_diary_public.rst index f7629da..699ea46 100644 --- a/docs/source/how_to/friends_diary.rst +++ b/docs/source/how_to/friends_diary_public.rst @@ -1,11 +1,15 @@ Accessing a Friend’s Diary ========================== -If a friend has their diary visibility set to public, you can grab their +If a friend has their diary visibility set to "Public", you can grab their diary entries: .. code:: python + import myfitnesspal + + client = myfitnesspal.Client() + friend_day = client.get_date(2020, 8, 23, username="username_of_my_friend") >>> friend_day <08/23/20 {'calories': 891.0, 'carbohydrates': 105.0, 'fat': 38.0, 'protein': 29.0, 'sodium': 0.0, 'sugar': 2.0}> diff --git a/docs/source/how_to/friends_diary_shared.rst b/docs/source/how_to/friends_diary_shared.rst new file mode 100644 index 0000000..15d5abe --- /dev/null +++ b/docs/source/how_to/friends_diary_shared.rst @@ -0,0 +1,54 @@ +Accessing a Friend’s Diary (Shared) +=================================== + +If a friend has their diary visibility set to "Friends Only", you can grab their +diary entries. + +To access a single day’s information: + +.. code:: python + + import myfitnesspal + + client = myfitnesspal.Client() + + friend_day = client.get_date(2020, 8, 23, friend_username="username_of_my_friend") + + friend_day + # >> <03/02/13 {'sodium': 3326, 'carbohydrates': 369, 'calories': 2001, 'fat': 22, 'sugar': 103, 'protein': 110}> + + friend_day.totals + # >> {'calories': 2001, + # 'carbohydrates': 369, + # 'fat': 22, + # 'protein': 110, + # 'sodium': 3326, + # 'sugar': 103} + + friend_day.meals + # >> [, + # , + # , + # ] + + +To access a week’s information with a loop on a date range: + +.. code:: python + + import datetime + + start_date = datetime.date(2023, 2, 18) + end_date = datetime.date(2023, 2, 25) + + friend_shared_diary_data = [] + + for day in range(int((end_date - start_date).days)): + loop_year=((start_date + datetime.timedelta(day)).strftime("%Y")) + loop_month=((start_date + datetime.timedelta(day)).strftime("%m")) + loop_day=((start_date + datetime.timedelta(day)).strftime("%d")) + loop_pretty_date=str(start_date + datetime.timedelta(day)) + diary_day_data = client.get_date(loop_year, loop_month, loop_day, friend_username="username_of_my_friend") + friend_shared_diary_data.append({loop_pretty_date: diary_day_data.totals}) + + print(friend_shared_diary_data) diff --git a/docs/source/how_to/index.rst b/docs/source/how_to/index.rst index 1fc02e9..829ed16 100644 --- a/docs/source/how_to/index.rst +++ b/docs/source/how_to/index.rst @@ -10,3 +10,5 @@ How-to Guides exercises measurements food_search + reports + use_with_wsl diff --git a/docs/source/how_to/measurements.rst b/docs/source/how_to/measurements.rst index 587619b..407e29a 100644 --- a/docs/source/how_to/measurements.rst +++ b/docs/source/how_to/measurements.rst @@ -7,7 +7,7 @@ To access measurements from the past 30 days: import myfitnesspal - client = myfitnesspal.Client('my_username') + client = myfitnesspal.Client() weight = client.get_measurements('Weight') weight diff --git a/docs/source/how_to/reports.rst b/docs/source/how_to/reports.rst new file mode 100644 index 0000000..0ce7ed0 --- /dev/null +++ b/docs/source/how_to/reports.rst @@ -0,0 +1,50 @@ +Accessing Reports +================= + +To access report data from the past 30 days: + +.. code:: python + + import myfitnesspal + + client = myfitnesspal.Client() + + client.get_report(report_name="Net Calories", report_category="Nutrition") + # >> OrderedDict([(datetime.date(2015, 5, 14), 1701.0), (datetime.date(2015, 5, 13), 1732.8), (datetime.date(2015, 5,12), 1721.8), + # (datetime.date(2015, 5, 11), 1701.6), (datetime.date(2015, 5, 10), 1272.4), (datetime.date(2015, 5, 9), 1720.2), + # (datetime.date(2015, 5, 8), 1071.0), (datetime.date(2015, 5, 7), 1721.2), (datetime.date(2015, 5, 6), 1270.8), + # (datetime.date(2015, 5, 5), 1701.8), (datetime.date(2015, 5, 4), 1724.2), (datetime.date(2015, 5, 3), 1722.2), + # (datetime.date(2015, 5, 2), 1701.0), (datetime.date(2015, 5, 1), 1721.2), (datetime.date(2015, 4, 30), 1721.6), + # (datetime.date(2015, 4, 29), 1072.4), (datetime.date(2015, 4, 28), 1272.2), (datetime.date(2015, 4, 27), 1723.2), + # (datetime.date(2015, 4, 26), 1791.8), (datetime.date(2015, 4, 25), 1720.8), (datetime.date(2015, 4, 24), 1721.2), + # (datetime.date(2015, 4, 23), 1721.6), (datetime.date(2015, 4, 22), 1723.2), (datetime.date(2015, 4, 21), 1724.2), + # (datetime.date(2015, 4, 20), 1273.6), (datetime.date(2015, 4, 19), 1721.8), (datetime.date(2015, 4, 18), 1720.4), + # (datetime.date(2015, 4, 17), 1629.8), (datetime.date(2015, 4, 16), 1270.4), (datetime.date(2015, 4, 15), 1270.8), + # (datetime.date(2015, 4, 14), 1721.6)]) + +.. code:: python + + import datetime + + may = datetime.date(2015, 5, 1) + + client.get_report("Net Calories", "Nutrition", may) + # >> OrderedDict([(datetime.date(2015, 5, 14), 172.8), (datetime.date(2015, 5, 13), 173.1), (datetime.date(2015, 5, 12), 127.7), + # (datetime.date(2015, 5, 11), 172.7), (datetime.date(2015, 5, 10), 172.8), (datetime.date(2015, 5, 9), 172.4), + # (datetime.date(2015, 5, 8), 172.6), (datetime.date(2015, 5, 7), 172.7), (datetime.date(2015, 5, 6), 172.6), + # (datetime.date(2015, 5, 5), 172.9), (datetime.date(2015, 5, 4), 173.0), (datetime.date(2015, 5, 3), 172.6), + # (datetime.date(2015, 5, 2), 172.6), (datetime.date(2015, 5, 1), 172.7)]) + +To access report data within a date range: + +.. code:: python + + thisweek = datetime.date(2015, 5, 11) + lastweek = datetime.date(2015, 5, 4) + + client.get_report("Net Calories", "Nutrition", thisweek, lastweek) + # >> OrderedDict([(datetime.date(2015, 5, 11), 1721.6), (datetime.date(2015, 5, 10), 1722.4), (datetime.date(2015, 5,9), 1720.2), + # (datetime.date(2015, 5, 8), 1271.0), (datetime.date(2015, 5, 7), 1721.2), (datetime.date(2015, 5, 6), 1720.8), + # (datetime.date(2015, 5, 5), 1721.8), (datetime.date(2015, 5, 4), 1274.2)]) + +Report data is returned as ordered dictionaries. The first argument specifies the report name, the second argument specifies the category name - both of which can be anything listed in the MyFitnessPal `Reports `_ page. When specifying a date range, the order of the date arguments does not matter. diff --git a/docs/source/how_to/use_with_wsl.rst b/docs/source/how_to/use_with_wsl.rst new file mode 100644 index 0000000..d0d2406 --- /dev/null +++ b/docs/source/how_to/use_with_wsl.rst @@ -0,0 +1,11 @@ +Use on Windows Subsystem for Linux +================================== + +When using python-myfitnesspal on Windows via Windows Subsystem for Linux +you will encounter one extra complication when logging in +due to the mechanism python-myfitnesspal uses +for gathering authentication credentials. + +You must log in to MyFitnessPal +via a browser installed on your guest (Linux) installation +instead of via one installed the conventional way. diff --git a/docs/source/index.rst b/docs/source/index.rst index 554dc70..1911809 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,6 +14,7 @@ Python Myfitnesspal how_to/index contributing api/index + upgrading Do you track your eating habits on `MyFitnessPal `_? Have you ever wanted to analyze the information you're entering into MyFitnessPal programatically? diff --git a/docs/source/upgrading.rst b/docs/source/upgrading.rst new file mode 100644 index 0000000..e530a40 --- /dev/null +++ b/docs/source/upgrading.rst @@ -0,0 +1,50 @@ +Upgrading from 1.x to 2.x +========================= + +Between the 1.x and 2.x versions of this library, +the mechanism used for authentication changed, +and this change has an impact +on how you instantiate the ``myfitnesspal.Client`` object. + +For more information about why this change was necessary, +see `Issue #144 `_. + +Version 1.x (Obsolete) +---------------------- + +Before getting started, +you would store a password in your system keyring +for the user account you would like to use +(in this example: 'myusername'). + +In your code, you would then instantiate your +``myfitnespal.Client`` like this: + +.. code:: python + + import myfitnesspal + + client = myfitnesspal.Client('myusername') + +Version 2.x (Current) +--------------------- + +Before getting started, +now you should open a web browser on the same computer +you will be using this library from, +go to `https://myfitnesspal.com/ `_, +and log in to MyFitnessPal using +the user account you would like to use. + +In your code, you can then instantiate your +``myfitnespal.Client`` like this: + +.. code:: python + + import myfitnesspal + + client = myfitnesspal.Client() + +Note that the instantiation no longer accepts a username, +and instead reads the log in information directly +from your browser. diff --git a/myfitnesspal/__init__.py b/myfitnesspal/__init__.py index 8a88388..4f53009 100644 --- a/myfitnesspal/__init__.py +++ b/myfitnesspal/__init__.py @@ -1,5 +1,5 @@ from myfitnesspal.client import Client # noqa -__version__ = "1.17.0" +__version__ = "2.1.2" VERSION = tuple(int(v) for v in __version__.split(".")) diff --git a/myfitnesspal/client.py b/myfitnesspal/client.py index f41046c..ec3b3e4 100644 --- a/myfitnesspal/client.py +++ b/myfitnesspal/client.py @@ -4,14 +4,18 @@ import json import logging import re +import uuid from collections import OrderedDict -from typing import Any, Dict, List, Optional, Union, cast, overload +from http.cookiejar import CookieJar +from pathlib import Path +from typing import Any, cast, overload +from urllib import parse +import browser_cookie3 import lxml.html import requests from measurement.base import MeasureBase from measurement.measures import Energy, Mass, Volume -from six.moves.urllib import parse from . import types from .base import MFPBase @@ -20,7 +24,6 @@ from .exceptions import MyfitnesspalLoginError, MyfitnesspalRequestFailed from .exercise import Exercise from .fooditem import FoodItem -from .keyring_utils import get_password_from_keyring from .meal import Meal from .note import Note @@ -32,6 +35,10 @@ class Client(MFPBase): """Provides access to MyFitnessPal APIs""" + COOKIE_DOMAINS = [ + "myfitnesspal.com", + "www.myfitnesspal.com", + ] BASE_URL = "http://www.myfitnesspal.com/" BASE_URL_SECURE = "https://www.myfitnesspal.com/" BASE_API_URL = "https://api.myfitnesspal.com/" @@ -56,19 +63,20 @@ class Client(MFPBase): def __init__( self, - username: str, - password: Optional[str] = None, - login: bool = True, + cookiejar: CookieJar | None = None, unit_aware: bool = False, + log_requests_to: Path | None = None, ): - self.provided_username = username - if password is None: - password = get_password_from_keyring(username) - self.__password = password - self.unit_aware = unit_aware + self._client_instance_id = uuid.uuid4() + self._request_counter = 0 + self._log_requests_to: Path | None = None + if log_requests_to: + self._log_requests_to = log_requests_to / Path( + str(self._client_instance_id) + ) + self._log_requests_to.mkdir(parents=True, exist_ok=True) - self._user_metadata: Optional[types.UserMetadata] = None - self._auth_data: Optional[types.AuthData] = None + self.unit_aware = unit_aware self.session = requests.Session() self.session.headers.update( @@ -76,11 +84,19 @@ def __init__( "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36" } ) - if login: - self._login() + if cookiejar is not None: + self.session.cookies.update(cookiejar) + else: + for domain_name in self.COOKIE_DOMAINS: + self.session.cookies.update( + browser_cookie3.load(domain_name=domain_name) + ) + + self._auth_data = self._get_auth_data() + self._user_metadata = self._get_user_metadata() @property - def user_id(self) -> Optional[types.MyfitnesspalUserId]: + def user_id(self) -> types.MyfitnesspalUserId | None: """The user_id of the logged-in account.""" if self._auth_data is None: return None @@ -88,12 +104,12 @@ def user_id(self) -> Optional[types.MyfitnesspalUserId]: return self._auth_data["user_id"] @property - def user_metadata(self) -> Optional[types.UserMetadata]: + def user_metadata(self) -> types.UserMetadata: """Metadata about of the logged-in account.""" return self._user_metadata @property - def access_token(self) -> Optional[str]: + def access_token(self) -> str | None: """The access token for the logged-in account.""" if self._auth_data is None: return None @@ -108,31 +124,7 @@ def effective_username(self) -> str: will fall back to the one provided if it is not. """ - if self.user_metadata: - return self.user_metadata["username"] - return self.provided_username - - def _login(self): - csrf_url = parse.urljoin(self.BASE_URL_SECURE, self.CSRF_PATH) - csrf_token = self._get_json_for_url(csrf_url)["csrfToken"] - - login_json_url = parse.urljoin(self.BASE_URL_SECURE, self.LOGIN_JSON_PATH) - - result = self.session.post( - login_json_url, - data={ - "csrfToken": csrf_token, - "username": self.effective_username, - "password": self.__password, - "redirect": False, - "json": True, - }, - ) - if "error=CredentialsSignin" in result.url: - raise MyfitnesspalLoginError() - - self._auth_data = self._get_auth_data() - self._user_metadata = self._get_user_metadata() + return self.user_metadata["username"] def _get_auth_data(self) -> types.AuthData: result = self._get_request_for_url( @@ -144,6 +136,15 @@ def _get_auth_data(self) -> types.AuthData: "status code: {status}".format(status=result.status_code) ) + if not result.headers["Content-Type"].startswith("application/json"): + # That we didn't receive a JSON document for this request + # is the only obvious clear signal that we aren't logged-in. + raise MyfitnesspalLoginError( + "Could not access MyFitnessPal using the cookies provided " + "by your browser. Are you sure you have logged in to " + "MyFitnessPal using a browser on this computer?" + ) + return result.json() def _get_user_metadata(self) -> types.UserMetadata: @@ -158,6 +159,10 @@ def _get_user_metadata(self) -> types.UserMetadata: "system_data", "profiles", "step_sources", + "privacy_preferences", + "social_preferences", + "app_preferences", + "partner_only_fields", ] query_string = parse.urlencode( [ @@ -190,26 +195,43 @@ def _get_full_name(self, raw_name: str) -> str: return name return self.ABBREVIATIONS[name] - def _get_url_for_date(self, date: datetime.date, username: str) -> str: + def _get_url_for_date( + self, date: datetime.date, username: str, friend_username=None + ) -> str: + if friend_username is not None: + name = friend_username + else: + name = username date_str = date.strftime("%Y-%m-%d") return ( - parse.urljoin(self.BASE_URL_SECURE, "food/diary/" + username) + parse.urljoin(self.BASE_URL_SECURE, "food/diary/" + name) + f"?date={date_str}" ) - def _get_url_for_measurements(self, page: int = 1, measurement_id: int = 1) -> str: + def _get_url_for_measurements( + self, page: int = 1, measurement_name: str = "" + ) -> str: return ( parse.urljoin(self.BASE_URL_SECURE, "measurements/edit") - + f"?page={page}&type={measurement_id}" + + "?" + + parse.urlencode({"page": page, "type": measurement_name}) ) def _get_request_for_url( self, url: str, send_token: bool = False, - headers: Optional[Dict[str, str]] = None, + headers: dict[str, str] | None = None, **kwargs, ) -> requests.Response: + request_id = uuid.uuid4() + self._request_counter += 1 + logger.debug( + "Sending request %s (#%s for client) to url %s", + self._request_counter, + request_id, + url, + ) if headers is None: headers = {} @@ -223,7 +245,38 @@ def _get_request_for_url( if self.user_id: headers["mfp-user-id"] = self.user_id - return self.session.get(url, headers=headers, **kwargs) + result = self.session.get(url, headers=headers, **kwargs) + if self._log_requests_to: + with open( + self._log_requests_to + / Path( + str(self._request_counter).zfill(3) + "__" + str(request_id) + ).with_suffix(".json"), + "w", + encoding="utf-8", + ) as outf: + outf.write( + json.dumps( + { + "request": { + "url": url, + "send_token": send_token, + "user_id": self.user_id if send_token else None, + "headers": headers, + "kwargs": kwargs, + }, + "response": { + "headers": dict(result.headers), + "status_code": result.status_code, + "content": result.content.decode("utf-8"), + }, + }, + indent=4, + sort_keys=True, + ) + ) + + return result def _get_content_for_url(self, *args, **kwargs) -> str: return self._get_request_for_url(*args, **kwargs).content.decode("utf8") @@ -238,7 +291,7 @@ def _get_json_for_url(self, url): return json.loads(content) - def _get_measurement(self, name: str, value: Optional[float]) -> MeasureBase: + def _get_measurement(self, name: str, value: float | None) -> MeasureBase: if not self.unit_aware: return value measure, kwarg = self.DEFAULT_MEASURE_AND_UNIT[name] @@ -252,7 +305,7 @@ def _get_numeric(self, string: str) -> float: ) else: try: - str_value = re.sub(r"[^\d.]+", "", string) + str_value = re.sub(r"[^-\d.]+", "", string) return float(str_value) except ValueError: return 0 @@ -303,7 +356,7 @@ def _get_completion(self, document) -> bool: return False # Who knows, probably not my diary. - def _get_meals(self, document) -> List[Meal]: + def _get_meals(self, document) -> list[Meal]: meals = [] fields = None meal_headers = document.xpath("//tr[@class='meal_header']") @@ -400,7 +453,6 @@ def _get_exercise(self, document): # If name is empty string: if columns[0].find("a") is None or not name: - # check for `td > div > a` if columns[0].find("div").find("a") is None: # then check for just `td > div` @@ -439,15 +491,15 @@ def _get_exercise(self, document): return exercises - def _get_exercises(self, date: datetime.date): + def _get_exercises(self, date: datetime.date, friend_username=None): + if friend_username is not None: + name = friend_username + else: + name = self.effective_username # get the exercise URL - document = self._get_document_for_url( - self._get_url_for_exercise(date, self.effective_username) - ) - + document = self._get_document_for_url(self._get_url_for_exercise(date, name)) # gather the exercise goals exercise = self._get_exercise(document) - return exercise def _extract_value(self, element): @@ -461,12 +513,10 @@ def _extract_value(self, element): return value @overload - def get_date(self, year: int, month: int, day: int) -> Day: - ... + def get_date(self, year: int, month: int, day: int) -> Day: ... @overload - def get_date(self, date: datetime.date) -> Day: - ... + def get_date(self, date: datetime.date) -> Day: ... def get_date(self, *args, **kwargs) -> Day: """Returns your meal diary for a particular date""" @@ -484,11 +534,23 @@ def get_date(self, *args, **kwargs) -> Day: "or three integers representing year, month, and day " "respectively." ) + friend_username = kwargs.get("friend_username") document = self._get_document_for_url( self._get_url_for_date( - date, kwargs.get("username", self.effective_username) + date, + kwargs.get("username", self.effective_username), + friend_username, ) ) + if "diary is locked with a key" in document.text_content(): + raise Exception("Error: diary is locked with a key") + if ( + friend_username is not None + and "user maintains a private diary" in document.text_content() + ): + raise Exception( + f"Error: Friend {kwargs.get('friend_username')}'s diary is private." + ) meals = self._get_meals(document) goals = self._get_goals(document) @@ -498,27 +560,29 @@ def get_date(self, *args, **kwargs) -> Day: # allow the day object to run the request if necessary. notes = lambda: self._get_notes(date) # noqa: E731 water = lambda: self._get_water(date) # noqa: E731 - exercises = lambda: self._get_exercises(date) # noqa: E731 - - day = Day( - date=date, - meals=meals, - goals=goals, - notes=notes, - water=water, - exercises=exercises, - complete=complete, - ) - + exercises = lambda: self._get_exercises(date, friend_username) # noqa: E731 + + if "friend_username" not in kwargs: + day = Day( + date=date, + meals=meals, + goals=goals, + notes=notes, + water=water, + exercises=exercises, + complete=complete, + ) + else: + day = Day( + date=date, + meals=meals, + goals=goals, + exercises=exercises, + complete=complete, + ) return day - def get_measurements( - self, - measurement="Weight", - lower_bound: Optional[datetime.date] = None, - upper_bound: Optional[datetime.date] = None, - ) -> Dict[datetime.date, float]: - """Returns measurements of a given name between two dates.""" + def _ensure_upper_lower_bound(self, lower_bound, upper_bound): if upper_bound is None: upper_bound = datetime.date.today() if lower_bound is None: @@ -528,6 +592,18 @@ def get_measurements( # just flip them around for them as a convenience if lower_bound > upper_bound: lower_bound, upper_bound = upper_bound, lower_bound + return upper_bound, lower_bound + + def get_measurements( + self, + measurement="Weight", + lower_bound: datetime.date | None = None, + upper_bound: datetime.date | None = None, + ) -> dict[datetime.date, float]: + """Returns measurements of a given name between two dates.""" + upper_bound, lower_bound = self._ensure_upper_lower_bound( + lower_bound, upper_bound + ) # get the URL for the main check in page document = self._get_document_for_url(self._get_url_for_measurements()) @@ -535,10 +611,7 @@ def get_measurements( # gather the IDs for all measurement types measurement_ids = self._get_measurement_ids(document) - # select the measurement ID based on the input - if measurement in measurement_ids.keys(): - measurement_id = measurement_ids[measurement] - else: + if measurement not in measurement_ids.keys(): raise ValueError(f"Measurement '{measurement}' does not exist.") page = 1 @@ -548,7 +621,7 @@ def get_measurements( while True: # retrieve the HTML from MyFitnessPal document = self._get_document_for_url( - self._get_url_for_measurements(page, measurement_id) + self._get_url_for_measurements(page, measurement) ) # parse the HTML for measurement entries and add to dictionary @@ -578,8 +651,8 @@ def get_measurements( def set_measurements( self, measurement="Weight", - value: float = None, - date: Optional[datetime.date] = None, + value: float | None = None, + date: datetime.date | None = None, ) -> None: """Sets measurement for today's date.""" if value is None: @@ -636,42 +709,39 @@ def set_measurements( ) def _get_measurements(self, document): - # find the tr element for each measurement entry on the page - trs = document.xpath("//table[contains(@class,'check-in')]/tbody/tr") - - measurements = OrderedDict() - - # create a dictionary out of the date and value of each entry - for entry in trs: + measurements = [] - # ensure there are measurement entries on the page - if len(entry) == 1: - return measurements - else: - measurements[entry[1].text] = entry[2].text + for next_data in document.xpath("//script[@id='__NEXT_DATA__']"): + next_data_json = json.loads(next_data.text) + for q in next_data_json["props"]["pageProps"]["dehydratedState"]["queries"]: + if "measurements" in q["queryKey"]: + if "items" in q["state"]["data"]: + measurements += q["state"]["data"]["items"] - temp_measurements = OrderedDict() + measurements_dict = OrderedDict() # converts the date to a datetime object and the value to a float - for date in measurements: - temp_measurements[ - datetime.datetime.strptime(date, "%m/%d/%Y").date() - ] = self._get_numeric(measurements[date]) - - measurements = temp_measurements - - return measurements - - def _get_measurement_ids(self, document) -> Dict[str, int]: + for entry in measurements: + date = datetime.datetime.strptime(entry["date"], "%Y-%m-%d").date() + if "unit" in entry: + value = f"{entry['value']} {entry['unit']}" + else: + value = f"{entry['value']}" + measurements_dict[date] = self._get_numeric(value) - # find the option element for all of the measurement choices - options = document.xpath("//select[@id='type']/option") + return measurements_dict + def _get_measurement_ids(self, document) -> dict[str, int]: ids = {} - - # create a dictionary out of the text and value of each choice - for option in options: - ids[option.text] = int(option.attrib.get("value")) + for next_data in document.xpath("//script[@id='__NEXT_DATA__']"): + next_data_json = json.loads(next_data.text) + for q in next_data_json["props"]["pageProps"]["dehydratedState"]["queries"]: + if "measurementTypes" in q["queryKey"]: + for m in q["state"]["data"]: + ids[m["description"]] = m["id"] + if "measurements" in q["queryKey"]: + if q["queryKey"][1] not in ids: + ids[q["queryKey"][1]] = "" return ids @@ -685,7 +755,7 @@ def _get_notes(self, date: datetime.date) -> Note: ) return Note(result.json()["item"]) - def _get_water(self, date: datetime.date) -> Union[float, Volume]: + def _get_water(self, date: datetime.date) -> float | Volume: result = self._get_request_for_url( parse.urljoin( self.BASE_URL_SECURE, @@ -699,22 +769,96 @@ def _get_water(self, date: datetime.date) -> Union[float, Volume]: return value + def get_report( + self, + report_name: str = "Net Calories", + report_category: str = "Nutrition", + lower_bound: datetime.date | None = None, + upper_bound: datetime.date | None = None, + ) -> dict[datetime.date, float]: + """ + Returns report data of a given name and category between two dates. + """ + if lower_bound and ((datetime.date.today() - lower_bound).days > 80): + logger.warning( + "Report API may not be able to look back this far. Some results may be incorrect." + ) + + upper_bound, lower_bound = self._ensure_upper_lower_bound( + lower_bound, upper_bound + ) + + assert upper_bound + assert lower_bound + + # Get the URL for the report + json_data = self._get_json_for_url( + self._get_url_for_report(report_name, report_category, lower_bound) + ) + + report = OrderedDict(self._get_report_data(json_data)) + + if not report: + raise ValueError("Could not load any results for the given category & name") + + # Remove entries that are not within the dates specified + for date in list(report.keys()): + if not upper_bound >= date >= lower_bound: + del report[date] + + return report + + def _get_url_for_report( + self, report_name: str, report_category: str, lower_bound: datetime.date + ) -> str: + delta = datetime.date.today() - lower_bound + return ( + parse.urljoin( + self.BASE_URL_SECURE, + "api/services/reports/results/" + + report_category.lower() + + "/" + + report_name, + ) + + f"/{str(delta.days)}.json" + ) + + def _get_report_data(self, json_data: dict) -> dict[datetime.date, float]: + report_data: dict[datetime.date, float] = {} + + data = json_data.get("outcome", {}).get("results") + + if not data: + return report_data + + for index, entry in enumerate(data): + # Dates are returned without year. + # As the returned dates will always begin from the current day, the + # correct date can be determined using the entry's index + date = ( + datetime.date.today() + - datetime.timedelta(days=len(data)) + + datetime.timedelta(days=index + 1) + ) + + report_data.update({date: entry["total"]}) + + return report_data + def __str__(self) -> str: return f"MyFitnessPal Client for {self.effective_username}" - def get_food_search_results(self, query: str) -> List[FoodItem]: + def get_food_search_results(self, query: str) -> list[FoodItem]: """Search for foods matching a specified query.""" search_url = parse.urljoin(self.BASE_URL_SECURE, self.SEARCH_PATH) document = self._get_document_for_url(search_url) authenticity_token = document.xpath( "(//input[@name='authenticity_token']/@value)[1]" )[0] - utf8_field = document.xpath("(//input[@name='utf8']/@value)[1]")[0] result = self.session.post( search_url, data={ - "utf8": utf8_field, "authenticity_token": authenticity_token, "search": query, "date": datetime.datetime.today().strftime("%Y-%m-%d"), @@ -731,7 +875,7 @@ def get_food_search_results(self, query: str) -> List[FoodItem]: return self._get_food_search_results(document) - def _get_food_search_results(self, document) -> List[FoodItem]: + def _get_food_search_results(self, document) -> list[FoodItem]: item_divs = document.xpath("//li[@class='matched-food']") items = [] @@ -745,15 +889,14 @@ def _get_food_search_results(self, document) -> List[FoodItem]: if item_div.xpath(".//div[@class='verified verified-list-icon']") else False ) - nutr_info = ( - item_div.xpath(".//p[@class='search-nutritional-info']")[0] - .text.strip() - .split(",") - ) + calories = None brand = "" - if len(nutr_info) >= 3: - brand = " ".join(nutr_info[0:-2]).strip() - calories = float(nutr_info[-1].replace("calories", "").strip()) + nutr_info_xpath = item_div.xpath(".//p[@class='search-nutritional-info']") + if nutr_info_xpath: + nutr_info = nutr_info_xpath[0].text.strip().split(",") + if len(nutr_info) >= 3: + brand = " ".join(nutr_info[0:-2]).strip() + calories = float(nutr_info[-1].replace("calories", "").strip()) items.append( FoodItem(mfp_id, mfp_name, brand, verif, calories, client=self) ) @@ -827,19 +970,19 @@ def set_new_food( fat: float, carbs: float, protein: float, - sodium: Optional[float] = None, - potassium: Optional[float] = None, - saturated_fat: Optional[float] = None, - polyunsaturated_fat: Optional[float] = None, - fiber: Optional[float] = None, - monounsaturated_fat: Optional[float] = None, - sugar: Optional[float] = None, - trans_fat: Optional[float] = None, - cholesterol: Optional[float] = None, - vitamin_a: Optional[float] = None, - calcium: Optional[float] = None, - vitamin_c: Optional[float] = None, - iron: Optional[float] = None, + sodium: float | None = None, + potassium: float | None = None, + saturated_fat: float | None = None, + polyunsaturated_fat: float | None = None, + fiber: float | None = None, + monounsaturated_fat: float | None = None, + sugar: float | None = None, + trans_fat: float | None = None, + cholesterol: float | None = None, + vitamin_a: float | None = None, + calcium: float | None = None, + vitamin_c: float | None = None, + iron: float | None = None, serving_size: str = "1 Serving", servingspercontainer: float = 1.0, sharepublic: bool = False, @@ -961,12 +1104,12 @@ def set_new_goal( self, energy: float, energy_unit: str = "calories", - carbohydrates: Optional[float] = None, - protein: Optional[float] = None, - fat: Optional[float] = None, - percent_carbohydrates: Optional[float] = None, - percent_protein: Optional[float] = None, - percent_fat: Optional[float] = None, + carbohydrates: float | None = None, + protein: float | None = None, + fat: float | None = None, + percent_carbohydrates: float | None = None, + percent_protein: float | None = None, + percent_fat: float | None = None, ) -> None: """Updates your nutrition goals. @@ -1116,7 +1259,7 @@ def set_new_goal( "status code: {status}".format(status=result.status_code) ) - def get_recipes(self) -> Dict[int, str]: + def get_recipes(self) -> dict[int, str]: """Returns a dictionary with all saved recipes. Recipe ID will be used as dictionary key, recipe title as dictionary value. @@ -1170,7 +1313,7 @@ def get_recipe(self, recipeid: int) -> types.Recipe: recipe_url = parse.urljoin(self.BASE_URL_SECURE, recipe_path) document = self._get_document_for_url(recipe_url) - recipe_dict: Dict[str, Any] = { + recipe_dict: dict[str, Any] = { "@context": "https://schema.org", "@type": "Recipe", "author": self.effective_username, @@ -1229,7 +1372,7 @@ def get_recipe(self, recipeid: int) -> types.Recipe: recipe_dict["tags"] = ["MyFitnessPal"] return cast(types.Recipe, recipe_dict) - def get_meals(self) -> Dict[int, str]: + def get_meals(self) -> dict[int, str]: """Returns a dictionary with all saved meals. Key: Meal ID @@ -1243,7 +1386,7 @@ def get_meals(self) -> Dict[int, str]: meals = document.xpath( "//*[@id='matching']/li" ) # get all items in the recipe list - _idx: Optional[int] = None + _idx: int | None = None try: for _idx, meal in enumerate(meals): meal_path = meal.xpath("./a")[0].attrib["href"] @@ -1266,7 +1409,7 @@ def get_meal(self, meal_id: int, meal_title: str) -> types.Recipe: meal_url = parse.urljoin(self.BASE_URL_SECURE, meal_path) document = self._get_document_for_url(meal_url) - recipe_dict: Dict[str, Any] = { + recipe_dict: dict[str, Any] = { "@context": "https://schema.org", "@type": "Recipe", "author": self.effective_username, diff --git a/myfitnesspal/cmdline.py b/myfitnesspal/cmdline.py index 4998745..95440ea 100644 --- a/myfitnesspal/cmdline.py +++ b/myfitnesspal/cmdline.py @@ -1,6 +1,7 @@ import argparse import logging import sys +from pathlib import Path from rich.console import Console from rich.logging import RichHandler @@ -20,6 +21,7 @@ def main(args=None): parser.add_argument("command", type=str, nargs=1, choices=COMMANDS.keys()) parser.add_argument("--loglevel", type=str, default="INFO") parser.add_argument("--traceback-locals", action="store_true") + parser.add_argument("--log-requests-to", type=Path, default=None) parser.add_argument("--debugger", action="store_true") args, extra = parser.parse_known_args() diff --git a/myfitnesspal/commands.py b/myfitnesspal/commands.py index e60aed1..58bd616 100644 --- a/myfitnesspal/commands.py +++ b/myfitnesspal/commands.py @@ -1,18 +1,12 @@ import argparse import logging from datetime import datetime -from getpass import getpass from typing import Dict from dateutil.parser import parse as dateparse from rich import print from . import Client -from .keyring_utils import ( - delete_password_in_keyring, - get_password_from_keyring_or_interactive, - store_password_in_keyring, -) from .types import CommandDefinition COMMANDS: Dict[str, CommandDefinition] = {} @@ -56,46 +50,11 @@ def decorator(fn): return decorator -@command( - "Store a MyFitnessPal password in your system keychain.", - aliases=["store-password"], -) -def store_password(args, *extra, **kwargs): - parser = argparse.ArgumentParser() - parser.add_argument( - "username", help="The MyFitnessPal username for which to store this password." - ) - args = parser.parse_args(extra) - - password = getpass(f"MyFitnessPal Password for {args.username}: ") - - store_password_in_keyring(args.username, password) - - -@command( - "Delete a MyFitnessPal password from your system keychain.", - aliases=["delete-password"], -) -def delete_password(args, *extra, **kwargs): - parser = argparse.ArgumentParser() - parser.add_argument( - "username", - help="The MyFitnessPal username for which to delete a stored password.", - ) - args = parser.parse_args(extra) - - delete_password_in_keyring(args.username) - - @command( "Display MyFitnessPal data for a given date.", ) -def day(args, *extra, **kwargs): +def day(super_args, *extra, **kwargs): parser = argparse.ArgumentParser() - parser.add_argument( - "username", - help="The MyFitnessPal username for which to delete a stored password.", - ) parser.add_argument( "date", nargs="?", @@ -105,8 +64,7 @@ def day(args, *extra, **kwargs): ) args = parser.parse_args(extra) - password = get_password_from_keyring_or_interactive(args.username) - client = Client(args.username, password) + client = Client(log_requests_to=super_args.log_requests_to) day = client.get_date(args.date) date_str = args.date.strftime("%Y-%m-%d") diff --git a/myfitnesspal/day.py b/myfitnesspal/day.py index 7e18f39..681603b 100644 --- a/myfitnesspal/day.py +++ b/myfitnesspal/day.py @@ -16,10 +16,10 @@ def __init__( self, date: datetime.date, meals: Optional[List[Meal]] = None, - goals: Dict[str, float] = None, - notes: Callable[[], str] = None, - water: Callable[[], float] = None, - exercises: Callable[[], List[Exercise]] = None, + goals: Optional[Dict[str, float]] = None, + notes: Optional[Callable[[], str]] = None, + water: Optional[Callable[[], float]] = None, + exercises: Optional[Callable[[], List[Exercise]]] = None, complete: bool = False, ): self._date = date diff --git a/myfitnesspal/fooditem.py b/myfitnesspal/fooditem.py index 3de175b..6a65730 100644 --- a/myfitnesspal/fooditem.py +++ b/myfitnesspal/fooditem.py @@ -21,7 +21,7 @@ def __init__( name: str, brand: Optional[str], verified: bool, - calories: float, + calories: Optional[float], details: Optional[FoodItemNutritionDict] = None, confirmations: Optional[int] = None, serving_sizes: Optional[List[types.ServingSizeDict]] = None, @@ -92,7 +92,7 @@ def serving(self) -> Optional[str]: return None @property - def calories(self) -> float: + def calories(self) -> Optional[float]: """Calories""" return self._calories diff --git a/myfitnesspal/keyring_utils.py b/myfitnesspal/keyring_utils.py deleted file mode 100644 index db81536..0000000 --- a/myfitnesspal/keyring_utils.py +++ /dev/null @@ -1,39 +0,0 @@ -import getpass - -import keyring - -KEYRING_SYSTEM = "python-myfitnesspal://myfitnesspal-password" - - -class NoStoredPasswordAvailable(Exception): - pass - - -def get_password_from_keyring(username: str) -> str: - result = keyring.get_password(KEYRING_SYSTEM, username) - if result is None: - raise NoStoredPasswordAvailable( - "No MyFitnessPal password for {username} could be found " - "in the system keychain. Use the `store-password` " - "command-line command for storing a password for this " - "username.".format( - username=username, - ) - ) - - return result - - -def store_password_in_keyring(username: str, password: str) -> None: - return keyring.set_password(KEYRING_SYSTEM, username, password) - - -def delete_password_in_keyring(username: str) -> None: - return keyring.delete_password(KEYRING_SYSTEM, username) - - -def get_password_from_keyring_or_interactive(username: str) -> str: - try: - return get_password_from_keyring(username) - except NoStoredPasswordAvailable: - return getpass.getpass(f"MyFitnessPal password for {username}: ") diff --git a/myfitnesspal/note.py b/myfitnesspal/note.py index 493fa8e..5ccdd41 100644 --- a/myfitnesspal/note.py +++ b/myfitnesspal/note.py @@ -2,7 +2,7 @@ import datetime from html import unescape -from typing import Dict, Optional +from typing import Optional from .types import NoteDataDict @@ -10,7 +10,7 @@ class Note(str): """Stores information about a note""" - _note_data: Dict[str, str] + _note_data: NoteDataDict def __new__(cls, note_data: NoteDataDict) -> Note: # I'm not sure I understand why this is double-encoded, but it is? diff --git a/readme.markdown b/readme.markdown index c3276c7..162debe 100644 --- a/readme.markdown +++ b/readme.markdown @@ -9,4 +9,4 @@ can be overcome using this library. - Having problems? [Issues live on github](https://github.com/coddingtonbear/python-myfitnesspal/issues). - Have questions? [Ask your questions in our gitter room](https://gitter.im/coddingtonbear/python-myfitnesspal) or [start a discussion](https://github.com/coddingtonbear/python-myfitnesspal/discussions/). -- Looking for docs? [Find them on RTD](https://python-myfitnesspal.readthedocs.io/). +- Looking for documentation and tutorials? [Find them on RTD](https://python-myfitnesspal.readthedocs.io/). diff --git a/requirements.txt b/requirements.txt index d92370c..607ef64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ blessed>=1.8.5,<2.0 -keyring>=8.0,<22.0 -lxml>=4.2.5,<5 +lxml>=5.0.2,<6 measurement>=3.2.0,<4.0 python-dateutil>=2.4,<3 requests>=2.17.0,<3 -rich>=9.10.0,<10 +rich>=12,<13 +browser_cookie3>=0.16.1,<1 +typing-extensions==4.12.2 diff --git a/setup.cfg b/setup.cfg index 7cffc38..9212aa1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,11 +2,18 @@ # https://github.com/ambv/black#line-length max-line-length = 88 ignore = - E203, # E203: whitespace before ':' (defer to black) - E231, # E231: missing whitespace after ',' (defer to black) - E501, # E501: line length (defer to black) - W503, # W503: break before binary operators (defer to black) - A003, # A003: [builtins] allow class attributes to be named after builtins (e.g., `id`) + # E203: whitespace before ':' (defer to black) + E203, + # E231: missing whitespace after ',' (defer to black) + E231, + # E501: line length (defer to black) + E501, + # W503: break before binary operators (defer to black) + W503, + # A003: [builtins] allow class attributes to be named after builtins (e.g., `id`) + A003, + # E704: Allow multiple statements on one line + E704, [pep8] max-line-length = 88 diff --git a/setup.py b/setup.py index 3ffd2a6..6062da5 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,14 @@ import multiprocessing # noqa -from setuptools import find_packages, setup +from setuptools import setup requirements = [] -with open("requirements.txt", "r") as in_: +with open("requirements.txt") as in_: requirements = in_.readlines() setup( name="myfitnesspal", - version="1.17.0", + version="2.1.2", url="http://github.com/coddingtonbear/python-myfitnesspal/", description="Access health and fitness data stored in Myfitnesspal", author="Adam Coddington", @@ -20,7 +20,7 @@ "Programming Language :: Python", "Topic :: Utilities", ], - packages=find_packages(), + packages=["myfitnesspal"], install_requires=requirements, test_suite="nose.collector", tests_require=[ diff --git a/tests/base.py b/tests/base.py index 88bf7f9..d45d9c3 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,5 +1,6 @@ import os import unittest +import json import lxml.html @@ -8,6 +9,12 @@ class MFPTestCase(unittest.TestCase): def get_html_document(self, file_name): file_path = os.path.join(os.path.dirname(__file__), "html", file_name) content = None - with open(file_path, "r") as in_: + with open(file_path, "r", encoding="utf-8") as in_: content = in_.read() return lxml.html.document_fromstring(content) + + def get_json_data(self, file_name): + file_path = os.path.join(os.path.dirname(__file__), "json", file_name) + with open(file_path, "r", encoding="utf-8") as in_: + content = in_.read() + return json.loads(content) diff --git a/tests/html/measurements.html b/tests/html/measurements.html index e9588e1..7e13458 100644 --- a/tests/html/measurements.html +++ b/tests/html/measurements.html @@ -1,979 +1,4236 @@ - - + + - - - - - - - Free Calorie Counter, Diet & Exercise Journal | MyFitnessPal.com - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+
+
+
+
- - - - -

Copyright 2005-2015 MyFitnessPal, Inc.

- - SheKnows Health & Beauty - -
-
-
-
- - - - - - - - - - - - - - - - - - +
+ - + \ No newline at end of file diff --git a/tests/json/report_nutrition_net_calories.json b/tests/json/report_nutrition_net_calories.json new file mode 100644 index 0000000..d940d02 --- /dev/null +++ b/tests/json/report_nutrition_net_calories.json @@ -0,0 +1,2813 @@ +{ + "chartType": "column", + "title": "Net Calories Consumed", + "category": "nutrition", + "label": "Net Calories Consumed", + "outcome": { + "results": [ + { + "date": "4/14", + "total": 0.0 + }, + { + "date": "4/15", + "total": 0.0 + }, + { + "date": "4/16", + "total": 0.0 + }, + { + "date": "4/17", + "total": 0.0 + }, + { + "date": "4/18", + "total": 0.0 + }, + { + "date": "4/19", + "total": 0.0 + }, + { + "date": "4/20", + "total": 0.0 + }, + { + "date": "4/21", + "total": 0.0 + }, + { + "date": "4/22", + "total": 0.0 + }, + { + "date": "4/23", + "total": 0.0 + }, + { + "date": "4/24", + "total": 0.0 + }, + { + "date": "4/25", + "total": 0.0 + }, + { + "date": "4/26", + "total": 0.0 + }, + { + "date": "4/27", + "total": 0.0 + }, + { + "date": "4/28", + "total": 0.0 + }, + { + "date": "4/29", + "total": 0.0 + }, + { + "date": "4/30", + "total": 0.0 + }, + { + "date": "5/01", + "total": 0.0 + }, + { + "date": "5/02", + "total": 0.0 + }, + { + "date": "5/03", + "total": 0.0 + }, + { + "date": "5/04", + "total": 0.0 + }, + { + "date": "5/05", + "total": 0.0 + }, + { + "date": "5/06", + "total": 0.0 + }, + { + "date": "5/07", + "total": 0.0 + }, + { + "date": "5/08", + "total": 0.0 + }, + { + "date": "5/09", + "total": 0.0 + }, + { + "date": "5/10", + "total": 0.0 + }, + { + "date": "5/11", + "total": 0.0 + }, + { + "date": "5/12", + "total": 0.0 + }, + { + "date": "5/13", + "total": 0.0 + }, + { + "date": "5/14", + "total": 0.0 + }, + { + "date": "5/15", + "total": 0.0 + }, + { + "date": "5/16", + "total": 0.0 + }, + { + "date": "5/17", + "total": 0.0 + }, + { + "date": "5/18", + "total": 0.0 + }, + { + "date": "5/19", + "total": 0.0 + }, + { + "date": "5/20", + "total": 0.0 + }, + { + "date": "5/21", + "total": 0.0 + }, + { + "date": "5/22", + "total": 0.0 + }, + { + "date": "5/23", + "total": 0.0 + }, + { + "date": "5/24", + "total": 0.0 + }, + { + "date": "5/25", + "total": 0.0 + }, + { + "date": "5/26", + "total": 0.0 + }, + { + "date": "5/27", + "total": 0.0 + }, + { + "date": "5/28", + "total": 0.0 + }, + { + "date": "5/29", + "total": 0.0 + }, + { + "date": "5/30", + "total": 0.0 + }, + { + "date": "5/31", + "total": 0.0 + }, + { + "date": "6/01", + "total": 0.0 + }, + { + "date": "6/02", + "total": 0.0 + }, + { + "date": "6/03", + "total": 0.0 + }, + { + "date": "6/04", + "total": 0.0 + }, + { + "date": "6/05", + "total": 0.0 + }, + { + "date": "6/06", + "total": 0.0 + }, + { + "date": "6/07", + "total": 0.0 + }, + { + "date": "6/08", + "total": 0.0 + }, + { + "date": "6/09", + "total": 0.0 + }, + { + "date": "6/10", + "total": 0.0 + }, + { + "date": "6/11", + "total": 0.0 + }, + { + "date": "6/12", + "total": 0.0 + }, + { + "date": "6/13", + "total": 0.0 + }, + { + "date": "6/14", + "total": 0.0 + }, + { + "date": "6/15", + "total": 0.0 + }, + { + "date": "6/16", + "total": 0.0 + }, + { + "date": "6/17", + "total": 0.0 + }, + { + "date": "6/18", + "total": 0.0 + }, + { + "date": "6/19", + "total": 0.0 + }, + { + "date": "6/20", + "total": 0.0 + }, + { + "date": "6/21", + "total": 0.0 + }, + { + "date": "6/22", + "total": 0.0 + }, + { + "date": "6/23", + "total": 0.0 + }, + { + "date": "6/24", + "total": 0.0 + }, + { + "date": "6/25", + "total": 0.0 + }, + { + "date": "6/26", + "total": 0.0 + }, + { + "date": "6/27", + "total": 0.0 + }, + { + "date": "6/28", + "total": 0.0 + }, + { + "date": "6/29", + "total": 0.0 + }, + { + "date": "6/30", + "total": 0.0 + }, + { + "date": "7/01", + "total": 0.0 + }, + { + "date": "7/02", + "total": 0.0 + }, + { + "date": "7/03", + "total": 0.0 + }, + { + "date": "7/04", + "total": 0.0 + }, + { + "date": "7/05", + "total": 0.0 + }, + { + "date": "7/06", + "total": 0.0 + }, + { + "date": "7/07", + "total": 0.0 + }, + { + "date": "7/08", + "total": 0.0 + }, + { + "date": "7/09", + "total": 0.0 + }, + { + "date": "7/10", + "total": 0.0 + }, + { + "date": "7/11", + "total": 0.0 + }, + { + "date": "7/12", + "total": 0.0 + }, + { + "date": "7/13", + "total": 0.0 + }, + { + "date": "7/14", + "total": 0.0 + }, + { + "date": "7/15", + "total": 0.0 + }, + { + "date": "7/16", + "total": 0.0 + }, + { + "date": "7/17", + "total": 0.0 + }, + { + "date": "7/18", + "total": 0.0 + }, + { + "date": "7/19", + "total": 0.0 + }, + { + "date": "7/20", + "total": 0.0 + }, + { + "date": "7/21", + "total": 0.0 + }, + { + "date": "7/22", + "total": 0.0 + }, + { + "date": "7/23", + "total": 0.0 + }, + { + "date": "7/24", + "total": 0.0 + }, + { + "date": "7/25", + "total": 0.0 + }, + { + "date": "7/26", + "total": 0.0 + }, + { + "date": "7/27", + "total": 0.0 + }, + { + "date": "7/28", + "total": 0.0 + }, + { + "date": "7/29", + "total": 0.0 + }, + { + "date": "7/30", + "total": 0.0 + }, + { + "date": "7/31", + "total": 0.0 + }, + { + "date": "8/01", + "total": 0.0 + }, + { + "date": "8/02", + "total": 0.0 + }, + { + "date": "8/03", + "total": 0.0 + }, + { + "date": "8/04", + "total": 0.0 + }, + { + "date": "8/05", + "total": 0.0 + }, + { + "date": "8/06", + "total": 0.0 + }, + { + "date": "8/07", + "total": 0.0 + }, + { + "date": "8/08", + "total": 0.0 + }, + { + "date": "8/09", + "total": 0.0 + }, + { + "date": "8/10", + "total": 0.0 + }, + { + "date": "8/11", + "total": 0.0 + }, + { + "date": "8/12", + "total": 0.0 + }, + { + "date": "8/13", + "total": 0.0 + }, + { + "date": "8/14", + "total": 0.0 + }, + { + "date": "8/15", + "total": 0.0 + }, + { + "date": "8/16", + "total": 0.0 + }, + { + "date": "8/17", + "total": 0.0 + }, + { + "date": "8/18", + "total": 0.0 + }, + { + "date": "8/19", + "total": 0.0 + }, + { + "date": "8/20", + "total": 0.0 + }, + { + "date": "8/21", + "total": 0.0 + }, + { + "date": "8/22", + "total": 0.0 + }, + { + "date": "8/23", + "total": 0.0 + }, + { + "date": "8/24", + "total": 0.0 + }, + { + "date": "8/25", + "total": 0.0 + }, + { + "date": "8/26", + "total": 0.0 + }, + { + "date": "8/27", + "total": 0.0 + }, + { + "date": "8/28", + "total": 0.0 + }, + { + "date": "8/29", + "total": 0.0 + }, + { + "date": "8/30", + "total": 0.0 + }, + { + "date": "8/31", + "total": 0.0 + }, + { + "date": "9/01", + "total": 0.0 + }, + { + "date": "9/02", + "total": 0.0 + }, + { + "date": "9/03", + "total": 0.0 + }, + { + "date": "9/04", + "total": 0.0 + }, + { + "date": "9/05", + "total": 0.0 + }, + { + "date": "9/06", + "total": 0.0 + }, + { + "date": "9/07", + "total": 0.0 + }, + { + "date": "9/08", + "total": 0.0 + }, + { + "date": "9/09", + "total": 0.0 + }, + { + "date": "9/10", + "total": 0.0 + }, + { + "date": "9/11", + "total": 0.0 + }, + { + "date": "9/12", + "total": 0.0 + }, + { + "date": "9/13", + "total": 0.0 + }, + { + "date": "9/14", + "total": 0.0 + }, + { + "date": "9/15", + "total": 0.0 + }, + { + "date": "9/16", + "total": 0.0 + }, + { + "date": "9/17", + "total": 0.0 + }, + { + "date": "9/18", + "total": 0.0 + }, + { + "date": "9/19", + "total": 0.0 + }, + { + "date": "9/20", + "total": 0.0 + }, + { + "date": "9/21", + "total": 0.0 + }, + { + "date": "9/22", + "total": 0.0 + }, + { + "date": "9/23", + "total": 0.0 + }, + { + "date": "9/24", + "total": 0.0 + }, + { + "date": "9/25", + "total": 0.0 + }, + { + "date": "9/26", + "total": 0.0 + }, + { + "date": "9/27", + "total": 0.0 + }, + { + "date": "9/28", + "total": 0.0 + }, + { + "date": "9/29", + "total": 0.0 + }, + { + "date": "9/30", + "total": 0.0 + }, + { + "date": "10/01", + "total": 0.0 + }, + { + "date": "10/02", + "total": 0.0 + }, + { + "date": "10/03", + "total": 0.0 + }, + { + "date": "10/04", + "total": 0.0 + }, + { + "date": "10/05", + "total": 0.0 + }, + { + "date": "10/06", + "total": 0.0 + }, + { + "date": "10/07", + "total": 0.0 + }, + { + "date": "10/08", + "total": 0.0 + }, + { + "date": "10/09", + "total": 0.0 + }, + { + "date": "10/10", + "total": 0.0 + }, + { + "date": "10/11", + "total": 0.0 + }, + { + "date": "10/12", + "total": 0.0 + }, + { + "date": "10/13", + "total": 0.0 + }, + { + "date": "10/14", + "total": 0.0 + }, + { + "date": "10/15", + "total": 0.0 + }, + { + "date": "10/16", + "total": 0.0 + }, + { + "date": "10/17", + "total": 0.0 + }, + { + "date": "10/18", + "total": 0.0 + }, + { + "date": "10/19", + "total": 0.0 + }, + { + "date": "10/20", + "total": 0.0 + }, + { + "date": "10/21", + "total": 0.0 + }, + { + "date": "10/22", + "total": 0.0 + }, + { + "date": "10/23", + "total": 0.0 + }, + { + "date": "10/24", + "total": 0.0 + }, + { + "date": "10/25", + "total": 0.0 + }, + { + "date": "10/26", + "total": 0.0 + }, + { + "date": "10/27", + "total": 0.0 + }, + { + "date": "10/28", + "total": 0.0 + }, + { + "date": "10/29", + "total": 0.0 + }, + { + "date": "10/30", + "total": 0.0 + }, + { + "date": "10/31", + "total": 0.0 + }, + { + "date": "11/01", + "total": 0.0 + }, + { + "date": "11/02", + "total": 0.0 + }, + { + "date": "11/03", + "total": 0.0 + }, + { + "date": "11/04", + "total": 0.0 + }, + { + "date": "11/05", + "total": 0.0 + }, + { + "date": "11/06", + "total": 0.0 + }, + { + "date": "11/07", + "total": 0.0 + }, + { + "date": "11/08", + "total": 0.0 + }, + { + "date": "11/09", + "total": 0.0 + }, + { + "date": "11/10", + "total": 0.0 + }, + { + "date": "11/11", + "total": 0.0 + }, + { + "date": "11/12", + "total": 0.0 + }, + { + "date": "11/13", + "total": 0.0 + }, + { + "date": "11/14", + "total": 0.0 + }, + { + "date": "11/15", + "total": 0.0 + }, + { + "date": "11/16", + "total": 0.0 + }, + { + "date": "11/17", + "total": 0.0 + }, + { + "date": "11/18", + "total": 0.0 + }, + { + "date": "11/19", + "total": 0.0 + }, + { + "date": "11/20", + "total": 0.0 + }, + { + "date": "11/21", + "total": 0.0 + }, + { + "date": "11/22", + "total": 0.0 + }, + { + "date": "11/23", + "total": 0.0 + }, + { + "date": "11/24", + "total": 0.0 + }, + { + "date": "11/25", + "total": 0.0 + }, + { + "date": "11/26", + "total": 0.0 + }, + { + "date": "11/27", + "total": 0.0 + }, + { + "date": "11/28", + "total": 0.0 + }, + { + "date": "11/29", + "total": 0.0 + }, + { + "date": "11/30", + "total": 0.0 + }, + { + "date": "12/01", + "total": 0.0 + }, + { + "date": "12/02", + "total": 0.0 + }, + { + "date": "12/03", + "total": 0.0 + }, + { + "date": "12/04", + "total": 0.0 + }, + { + "date": "12/05", + "total": 0.0 + }, + { + "date": "12/06", + "total": 0.0 + }, + { + "date": "12/07", + "total": 0.0 + }, + { + "date": "12/08", + "total": 0.0 + }, + { + "date": "12/09", + "total": 0.0 + }, + { + "date": "12/10", + "total": 0.0 + }, + { + "date": "12/11", + "total": 0.0 + }, + { + "date": "12/12", + "total": 0.0 + }, + { + "date": "12/13", + "total": 0.0 + }, + { + "date": "12/14", + "total": 0.0 + }, + { + "date": "12/15", + "total": 0.0 + }, + { + "date": "12/16", + "total": 0.0 + }, + { + "date": "12/17", + "total": 0.0 + }, + { + "date": "12/18", + "total": 0.0 + }, + { + "date": "12/19", + "total": 0.0 + }, + { + "date": "12/20", + "total": 0.0 + }, + { + "date": "12/21", + "total": 0.0 + }, + { + "date": "12/22", + "total": 0.0 + }, + { + "date": "12/23", + "total": 0.0 + }, + { + "date": "12/24", + "total": 0.0 + }, + { + "date": "12/25", + "total": 0.0 + }, + { + "date": "12/26", + "total": 0.0 + }, + { + "date": "12/27", + "total": 0.0 + }, + { + "date": "12/28", + "total": 0.0 + }, + { + "date": "12/29", + "total": 0.0 + }, + { + "date": "12/30", + "total": 0.0 + }, + { + "date": "12/31", + "total": 0.0 + }, + { + "date": "1/01", + "total": 0.0 + }, + { + "date": "1/02", + "total": 1372.0 + }, + { + "date": "1/03", + "total": 3237.0 + }, + { + "date": "1/04", + "total": -49.0 + }, + { + "date": "1/05", + "total": -83.0 + }, + { + "date": "1/06", + "total": 1221.0 + }, + { + "date": "1/07", + "total": -799.0 + }, + { + "date": "1/08", + "total": -513.0 + }, + { + "date": "1/09", + "total": 217.0 + }, + { + "date": "1/10", + "total": -832.0 + }, + { + "date": "1/11", + "total": -908.0 + }, + { + "date": "1/12", + "total": -739.0 + }, + { + "date": "1/13", + "total": 1152.0 + }, + { + "date": "1/14", + "total": -841.0 + }, + { + "date": "1/15", + "total": -246.0 + }, + { + "date": "1/16", + "total": -633.0 + }, + { + "date": "1/17", + "total": -709.0 + }, + { + "date": "1/18", + "total": -1270.0 + }, + { + "date": "1/19", + "total": -123.0 + }, + { + "date": "1/20", + "total": -832.0 + }, + { + "date": "1/21", + "total": -866.0 + }, + { + "date": "1/22", + "total": -203.0 + }, + { + "date": "1/23", + "total": -785.0 + }, + { + "date": "1/24", + "total": -923.0 + }, + { + "date": "1/25", + "total": -638.0 + }, + { + "date": "1/26", + "total": -80.0 + }, + { + "date": "1/27", + "total": -906.0 + }, + { + "date": "1/28", + "total": -693.0 + }, + { + "date": "1/29", + "total": -150.0 + }, + { + "date": "1/30", + "total": -783.0 + }, + { + "date": "1/31", + "total": -437.0 + }, + { + "date": "2/01", + "total": -299.0 + }, + { + "date": "2/02", + "total": 0.0 + }, + { + "date": "2/03", + "total": 0.0 + }, + { + "date": "2/04", + "total": 0.0 + }, + { + "date": "2/05", + "total": 0.0 + }, + { + "date": "2/06", + "total": 0.0 + }, + { + "date": "2/07", + "total": 0.0 + }, + { + "date": "2/08", + "total": 0.0 + }, + { + "date": "2/09", + "total": 0.0 + }, + { + "date": "2/10", + "total": 0.0 + }, + { + "date": "2/11", + "total": 0.0 + }, + { + "date": "2/12", + "total": 0.0 + }, + { + "date": "2/13", + "total": -10.0 + }, + { + "date": "2/14", + "total": 0.0 + }, + { + "date": "2/15", + "total": 0.0 + }, + { + "date": "2/16", + "total": 0.0 + }, + { + "date": "2/17", + "total": 0.0 + }, + { + "date": "2/18", + "total": 0.0 + }, + { + "date": "2/19", + "total": 0.0 + }, + { + "date": "2/20", + "total": 0.0 + }, + { + "date": "2/21", + "total": 0.0 + }, + { + "date": "2/22", + "total": 0.0 + }, + { + "date": "2/23", + "total": 0.0 + }, + { + "date": "2/24", + "total": 0.0 + }, + { + "date": "2/25", + "total": 0.0 + }, + { + "date": "2/26", + "total": 0.0 + }, + { + "date": "2/27", + "total": 0.0 + }, + { + "date": "2/28", + "total": 0.0 + }, + { + "date": "2/29", + "total": 0.0 + }, + { + "date": "3/01", + "total": 0.0 + }, + { + "date": "3/02", + "total": 0.0 + }, + { + "date": "3/03", + "total": 0.0 + }, + { + "date": "3/04", + "total": 0.0 + }, + { + "date": "3/05", + "total": 0.0 + }, + { + "date": "3/06", + "total": 0.0 + }, + { + "date": "3/07", + "total": 0.0 + }, + { + "date": "3/08", + "total": 0.0 + }, + { + "date": "3/09", + "total": 0.0 + }, + { + "date": "3/10", + "total": 0.0 + }, + { + "date": "3/11", + "total": 0.0 + }, + { + "date": "3/12", + "total": 0.0 + }, + { + "date": "3/13", + "total": 0.0 + }, + { + "date": "3/14", + "total": 0.0 + }, + { + "date": "3/15", + "total": 0.0 + }, + { + "date": "3/16", + "total": 0.0 + }, + { + "date": "3/17", + "total": 0.0 + }, + { + "date": "3/18", + "total": 0.0 + }, + { + "date": "3/19", + "total": 0.0 + }, + { + "date": "3/20", + "total": 0.0 + }, + { + "date": "3/21", + "total": 0.0 + }, + { + "date": "3/22", + "total": 0.0 + }, + { + "date": "3/23", + "total": 0.0 + }, + { + "date": "3/24", + "total": 0.0 + }, + { + "date": "3/25", + "total": 0.0 + }, + { + "date": "3/26", + "total": 0.0 + }, + { + "date": "3/27", + "total": 0.0 + }, + { + "date": "3/28", + "total": 0.0 + }, + { + "date": "3/29", + "total": 0.0 + }, + { + "date": "3/30", + "total": 0.0 + }, + { + "date": "3/31", + "total": 0.0 + }, + { + "date": "4/01", + "total": -218.0 + }, + { + "date": "4/02", + "total": -297.0 + }, + { + "date": "4/03", + "total": -400.0 + }, + { + "date": "4/04", + "total": -272.0 + }, + { + "date": "4/05", + "total": -304.0 + }, + { + "date": "4/06", + "total": -3.0 + }, + { + "date": "4/07", + "total": 0.0 + }, + { + "date": "4/08", + "total": 0.0 + }, + { + "date": "4/09", + "total": 0.0 + }, + { + "date": "4/10", + "total": 0.0 + }, + { + "date": "4/11", + "total": 0.0 + }, + { + "date": "4/12", + "total": 0.0 + }, + { + "date": "4/13", + "total": 0.0 + }, + { + "date": "4/14", + "total": 0.0 + }, + { + "date": "4/15", + "total": 0.0 + }, + { + "date": "4/16", + "total": 0.0 + }, + { + "date": "4/17", + "total": 0.0 + }, + { + "date": "4/18", + "total": 0.0 + }, + { + "date": "4/19", + "total": 0.0 + }, + { + "date": "4/20", + "total": 0.0 + }, + { + "date": "4/21", + "total": 0.0 + }, + { + "date": "4/22", + "total": 0.0 + }, + { + "date": "4/23", + "total": 0.0 + }, + { + "date": "4/24", + "total": 0.0 + }, + { + "date": "4/25", + "total": 0.0 + }, + { + "date": "4/26", + "total": 0.0 + }, + { + "date": "4/27", + "total": 0.0 + }, + { + "date": "4/28", + "total": 0.0 + }, + { + "date": "4/29", + "total": 0.0 + }, + { + "date": "4/30", + "total": 0.0 + }, + { + "date": "5/01", + "total": 0.0 + }, + { + "date": "5/02", + "total": 0.0 + }, + { + "date": "5/03", + "total": 0.0 + }, + { + "date": "5/04", + "total": 0.0 + }, + { + "date": "5/05", + "total": 0.0 + }, + { + "date": "5/06", + "total": 0.0 + }, + { + "date": "5/07", + "total": 0.0 + }, + { + "date": "5/08", + "total": 0.0 + }, + { + "date": "5/09", + "total": 0.0 + }, + { + "date": "5/10", + "total": 0.0 + }, + { + "date": "5/11", + "total": 0.0 + }, + { + "date": "5/12", + "total": 0.0 + }, + { + "date": "5/13", + "total": 0.0 + }, + { + "date": "5/14", + "total": 0.0 + }, + { + "date": "5/15", + "total": 0.0 + }, + { + "date": "5/16", + "total": 0.0 + }, + { + "date": "5/17", + "total": 0.0 + }, + { + "date": "5/18", + "total": 0.0 + }, + { + "date": "5/19", + "total": 0.0 + }, + { + "date": "5/20", + "total": 0.0 + }, + { + "date": "5/21", + "total": 0.0 + }, + { + "date": "5/22", + "total": 0.0 + }, + { + "date": "5/23", + "total": 0.0 + }, + { + "date": "5/24", + "total": 0.0 + }, + { + "date": "5/25", + "total": 0.0 + }, + { + "date": "5/26", + "total": 0.0 + }, + { + "date": "5/27", + "total": 0.0 + }, + { + "date": "5/28", + "total": 0.0 + }, + { + "date": "5/29", + "total": 0.0 + }, + { + "date": "5/30", + "total": 0.0 + }, + { + "date": "5/31", + "total": 0.0 + }, + { + "date": "6/01", + "total": 0.0 + }, + { + "date": "6/02", + "total": 0.0 + }, + { + "date": "6/03", + "total": 0.0 + }, + { + "date": "6/04", + "total": 0.0 + }, + { + "date": "6/05", + "total": 0.0 + }, + { + "date": "6/06", + "total": 0.0 + }, + { + "date": "6/07", + "total": 0.0 + }, + { + "date": "6/08", + "total": 0.0 + }, + { + "date": "6/09", + "total": 0.0 + }, + { + "date": "6/10", + "total": 0.0 + }, + { + "date": "6/11", + "total": 0.0 + }, + { + "date": "6/12", + "total": 0.0 + }, + { + "date": "6/13", + "total": 0.0 + }, + { + "date": "6/14", + "total": 0.0 + }, + { + "date": "6/15", + "total": 0.0 + }, + { + "date": "6/16", + "total": 0.0 + }, + { + "date": "6/17", + "total": 0.0 + }, + { + "date": "6/18", + "total": 0.0 + }, + { + "date": "6/19", + "total": 0.0 + }, + { + "date": "6/20", + "total": 0.0 + }, + { + "date": "6/21", + "total": 0.0 + }, + { + "date": "6/22", + "total": 0.0 + }, + { + "date": "6/23", + "total": 0.0 + }, + { + "date": "6/24", + "total": 0.0 + }, + { + "date": "6/25", + "total": 0.0 + }, + { + "date": "6/26", + "total": 0.0 + }, + { + "date": "6/27", + "total": 0.0 + }, + { + "date": "6/28", + "total": 0.0 + }, + { + "date": "6/29", + "total": 0.0 + }, + { + "date": "6/30", + "total": 0.0 + }, + { + "date": "7/01", + "total": 0.0 + }, + { + "date": "7/02", + "total": 0.0 + }, + { + "date": "7/03", + "total": 0.0 + }, + { + "date": "7/04", + "total": 0.0 + }, + { + "date": "7/05", + "total": 0.0 + }, + { + "date": "7/06", + "total": 0.0 + }, + { + "date": "7/07", + "total": 0.0 + }, + { + "date": "7/08", + "total": 0.0 + }, + { + "date": "7/09", + "total": 0.0 + }, + { + "date": "7/10", + "total": 0.0 + }, + { + "date": "7/11", + "total": 0.0 + }, + { + "date": "7/12", + "total": 0.0 + }, + { + "date": "7/13", + "total": 0.0 + }, + { + "date": "7/14", + "total": 0.0 + }, + { + "date": "7/15", + "total": 0.0 + }, + { + "date": "7/16", + "total": 0.0 + }, + { + "date": "7/17", + "total": 0.0 + }, + { + "date": "7/18", + "total": 0.0 + }, + { + "date": "7/19", + "total": 0.0 + }, + { + "date": "7/20", + "total": 0.0 + }, + { + "date": "7/21", + "total": 0.0 + }, + { + "date": "7/22", + "total": 0.0 + }, + { + "date": "7/23", + "total": 0.0 + }, + { + "date": "7/24", + "total": 0.0 + }, + { + "date": "7/25", + "total": 0.0 + }, + { + "date": "7/26", + "total": 0.0 + }, + { + "date": "7/27", + "total": 0.0 + }, + { + "date": "7/28", + "total": 0.0 + }, + { + "date": "7/29", + "total": 0.0 + }, + { + "date": "7/30", + "total": 0.0 + }, + { + "date": "7/31", + "total": 0.0 + }, + { + "date": "8/01", + "total": 0.0 + }, + { + "date": "8/02", + "total": 0.0 + }, + { + "date": "8/03", + "total": 0.0 + }, + { + "date": "8/04", + "total": 0.0 + }, + { + "date": "8/05", + "total": 0.0 + }, + { + "date": "8/06", + "total": 0.0 + }, + { + "date": "8/07", + "total": 0.0 + }, + { + "date": "8/08", + "total": 0.0 + }, + { + "date": "8/09", + "total": 0.0 + }, + { + "date": "8/10", + "total": 0.0 + }, + { + "date": "8/11", + "total": 0.0 + }, + { + "date": "8/12", + "total": 0.0 + }, + { + "date": "8/13", + "total": 0.0 + }, + { + "date": "8/14", + "total": 0.0 + }, + { + "date": "8/15", + "total": 0.0 + }, + { + "date": "8/16", + "total": 0.0 + }, + { + "date": "8/17", + "total": 0.0 + }, + { + "date": "8/18", + "total": 0.0 + }, + { + "date": "8/19", + "total": 0.0 + }, + { + "date": "8/20", + "total": 0.0 + }, + { + "date": "8/21", + "total": 0.0 + }, + { + "date": "8/22", + "total": 0.0 + }, + { + "date": "8/23", + "total": 0.0 + }, + { + "date": "8/24", + "total": 0.0 + }, + { + "date": "8/25", + "total": 0.0 + }, + { + "date": "8/26", + "total": 0.0 + }, + { + "date": "8/27", + "total": 0.0 + }, + { + "date": "8/28", + "total": 0.0 + }, + { + "date": "8/29", + "total": 0.0 + }, + { + "date": "8/30", + "total": 0.0 + }, + { + "date": "8/31", + "total": 0.0 + }, + { + "date": "9/01", + "total": 0.0 + }, + { + "date": "9/02", + "total": 0.0 + }, + { + "date": "9/03", + "total": 0.0 + }, + { + "date": "9/04", + "total": 0.0 + }, + { + "date": "9/05", + "total": 0.0 + }, + { + "date": "9/06", + "total": 0.0 + }, + { + "date": "9/07", + "total": 0.0 + }, + { + "date": "9/08", + "total": 0.0 + }, + { + "date": "9/09", + "total": 0.0 + }, + { + "date": "9/10", + "total": 0.0 + }, + { + "date": "9/11", + "total": 0.0 + }, + { + "date": "9/12", + "total": 0.0 + }, + { + "date": "9/13", + "total": 0.0 + }, + { + "date": "9/14", + "total": 0.0 + }, + { + "date": "9/15", + "total": 0.0 + }, + { + "date": "9/16", + "total": 0.0 + }, + { + "date": "9/17", + "total": 0.0 + }, + { + "date": "9/18", + "total": 0.0 + }, + { + "date": "9/19", + "total": 0.0 + }, + { + "date": "9/20", + "total": 0.0 + }, + { + "date": "9/21", + "total": 0.0 + }, + { + "date": "9/22", + "total": 0.0 + }, + { + "date": "9/23", + "total": 0.0 + }, + { + "date": "9/24", + "total": 0.0 + }, + { + "date": "9/25", + "total": 0.0 + }, + { + "date": "9/26", + "total": 0.0 + }, + { + "date": "9/27", + "total": 0.0 + }, + { + "date": "9/28", + "total": 0.0 + }, + { + "date": "9/29", + "total": 0.0 + }, + { + "date": "9/30", + "total": 0.0 + }, + { + "date": "10/01", + "total": 0.0 + }, + { + "date": "10/02", + "total": 0.0 + }, + { + "date": "10/03", + "total": 0.0 + }, + { + "date": "10/04", + "total": 0.0 + }, + { + "date": "10/05", + "total": 0.0 + }, + { + "date": "10/06", + "total": 0.0 + }, + { + "date": "10/07", + "total": 0.0 + }, + { + "date": "10/08", + "total": 0.0 + }, + { + "date": "10/09", + "total": 0.0 + }, + { + "date": "10/10", + "total": 0.0 + }, + { + "date": "10/11", + "total": -287.0 + }, + { + "date": "10/12", + "total": -30.0 + }, + { + "date": "10/13", + "total": -81.0 + }, + { + "date": "10/14", + "total": -107.0 + }, + { + "date": "10/15", + "total": -95.0 + }, + { + "date": "10/16", + "total": -156.0 + }, + { + "date": "10/17", + "total": -81.0 + }, + { + "date": "10/18", + "total": 239.0 + }, + { + "date": "10/19", + "total": -14.0 + }, + { + "date": "10/20", + "total": -35.0 + }, + { + "date": "10/21", + "total": -283.0 + }, + { + "date": "10/22", + "total": -56.0 + }, + { + "date": "10/23", + "total": -91.0 + }, + { + "date": "10/24", + "total": -217.0 + }, + { + "date": "10/25", + "total": -311.0 + }, + { + "date": "10/26", + "total": -168.0 + }, + { + "date": "10/27", + "total": -70.0 + }, + { + "date": "10/28", + "total": -41.0 + }, + { + "date": "10/29", + "total": -45.0 + }, + { + "date": "10/30", + "total": -36.0 + }, + { + "date": "10/31", + "total": 0.0 + }, + { + "date": "11/01", + "total": 0.0 + }, + { + "date": "11/02", + "total": 0.0 + }, + { + "date": "11/03", + "total": 0.0 + }, + { + "date": "11/04", + "total": 0.0 + }, + { + "date": "11/05", + "total": 0.0 + }, + { + "date": "11/06", + "total": 0.0 + }, + { + "date": "11/07", + "total": 0.0 + }, + { + "date": "11/08", + "total": 0.0 + }, + { + "date": "11/09", + "total": 0.0 + }, + { + "date": "11/10", + "total": -425.0 + }, + { + "date": "11/11", + "total": 0.0 + }, + { + "date": "11/12", + "total": 0.0 + }, + { + "date": "11/13", + "total": 0.0 + }, + { + "date": "11/14", + "total": 0.0 + }, + { + "date": "11/15", + "total": 0.0 + }, + { + "date": "11/16", + "total": 0.0 + }, + { + "date": "11/17", + "total": 0.0 + }, + { + "date": "11/18", + "total": 0.0 + }, + { + "date": "11/19", + "total": 0.0 + }, + { + "date": "11/20", + "total": 0.0 + }, + { + "date": "11/21", + "total": 0.0 + }, + { + "date": "11/22", + "total": 0.0 + }, + { + "date": "11/23", + "total": 0.0 + }, + { + "date": "11/24", + "total": 0.0 + }, + { + "date": "11/25", + "total": 0.0 + }, + { + "date": "11/26", + "total": 0.0 + }, + { + "date": "11/27", + "total": 0.0 + }, + { + "date": "11/28", + "total": 0.0 + }, + { + "date": "11/29", + "total": 0.0 + }, + { + "date": "11/30", + "total": 0.0 + }, + { + "date": "12/01", + "total": 0.0 + }, + { + "date": "12/02", + "total": 0.0 + }, + { + "date": "12/03", + "total": 0.0 + }, + { + "date": "12/04", + "total": 0.0 + }, + { + "date": "12/05", + "total": 0.0 + }, + { + "date": "12/06", + "total": 0.0 + }, + { + "date": "12/07", + "total": 0.0 + }, + { + "date": "12/08", + "total": 0.0 + }, + { + "date": "12/09", + "total": 0.0 + }, + { + "date": "12/10", + "total": 0.0 + }, + { + "date": "12/11", + "total": 0.0 + }, + { + "date": "12/12", + "total": 0.0 + }, + { + "date": "12/13", + "total": 0.0 + }, + { + "date": "12/14", + "total": 0.0 + }, + { + "date": "12/15", + "total": 0.0 + }, + { + "date": "12/16", + "total": 0.0 + }, + { + "date": "12/17", + "total": 0.0 + }, + { + "date": "12/18", + "total": 0.0 + }, + { + "date": "12/19", + "total": 0.0 + }, + { + "date": "12/20", + "total": 0.0 + }, + { + "date": "12/21", + "total": 0.0 + }, + { + "date": "12/22", + "total": 0.0 + }, + { + "date": "12/23", + "total": 0.0 + }, + { + "date": "12/24", + "total": 0.0 + }, + { + "date": "12/25", + "total": 0.0 + }, + { + "date": "12/26", + "total": 0.0 + }, + { + "date": "12/27", + "total": 0.0 + }, + { + "date": "12/28", + "total": 0.0 + }, + { + "date": "12/29", + "total": 0.0 + }, + { + "date": "12/30", + "total": 0.0 + }, + { + "date": "12/31", + "total": 0.0 + }, + { + "date": "1/01", + "total": 0.0 + }, + { + "date": "1/02", + "total": 0.0 + }, + { + "date": "1/03", + "total": 0.0 + }, + { + "date": "1/04", + "total": 0.0 + }, + { + "date": "1/05", + "total": 0.0 + }, + { + "date": "1/06", + "total": 0.0 + }, + { + "date": "1/07", + "total": 0.0 + }, + { + "date": "1/08", + "total": 0.0 + }, + { + "date": "1/09", + "total": 0.0 + }, + { + "date": "1/10", + "total": 0.0 + }, + { + "date": "1/11", + "total": 0.0 + }, + { + "date": "1/12", + "total": 0.0 + }, + { + "date": "1/13", + "total": 0.0 + }, + { + "date": "1/14", + "total": 0.0 + }, + { + "date": "1/15", + "total": 0.0 + }, + { + "date": "1/16", + "total": 0.0 + }, + { + "date": "1/17", + "total": 0.0 + }, + { + "date": "1/18", + "total": 0.0 + }, + { + "date": "1/19", + "total": 0.0 + }, + { + "date": "1/20", + "total": 0.0 + }, + { + "date": "1/21", + "total": 0.0 + }, + { + "date": "1/22", + "total": 0.0 + }, + { + "date": "1/23", + "total": 0.0 + }, + { + "date": "1/24", + "total": 0.0 + }, + { + "date": "1/25", + "total": 0.0 + }, + { + "date": "1/26", + "total": 0.0 + }, + { + "date": "1/27", + "total": 0.0 + }, + { + "date": "1/28", + "total": 0.0 + }, + { + "date": "1/29", + "total": 0.0 + }, + { + "date": "1/30", + "total": 0.0 + }, + { + "date": "1/31", + "total": 0.0 + }, + { + "date": "2/01", + "total": 0.0 + }, + { + "date": "2/02", + "total": 0.0 + }, + { + "date": "2/03", + "total": 0.0 + }, + { + "date": "2/04", + "total": 0.0 + }, + { + "date": "2/05", + "total": 0.0 + }, + { + "date": "2/06", + "total": 0.0 + }, + { + "date": "2/07", + "total": 0.0 + }, + { + "date": "2/08", + "total": 0.0 + }, + { + "date": "2/09", + "total": 0.0 + }, + { + "date": "2/10", + "total": 0.0 + }, + { + "date": "2/11", + "total": 0.0 + }, + { + "date": "2/12", + "total": 0.0 + }, + { + "date": "2/13", + "total": 0.0 + }, + { + "date": "2/14", + "total": 0.0 + }, + { + "date": "2/15", + "total": 0.0 + }, + { + "date": "2/16", + "total": -21.0 + }, + { + "date": "2/17", + "total": -22.0 + }, + { + "date": "2/18", + "total": 0.0 + }, + { + "date": "2/19", + "total": -26.0 + }, + { + "date": "2/20", + "total": -405.0 + }, + { + "date": "2/21", + "total": -176.0 + }, + { + "date": "2/22", + "total": -29.0 + }, + { + "date": "2/23", + "total": 1419.0 + }, + { + "date": "2/24", + "total": 1442.0 + }, + { + "date": "2/25", + "total": 1312.0 + }, + { + "date": "2/26", + "total": 1424.0 + }, + { + "date": "2/27", + "total": 1575.0 + }, + { + "date": "2/28", + "total": 985.0 + }, + { + "date": "3/01", + "total": 1510.0 + }, + { + "date": "3/02", + "total": 1272.0 + }, + { + "date": "3/03", + "total": 1409.0 + }, + { + "date": "3/04", + "total": 1168.0 + }, + { + "date": "3/05", + "total": 1581.0 + }, + { + "date": "3/06", + "total": 1416.0 + }, + { + "date": "3/07", + "total": 1312.0 + }, + { + "date": "3/08", + "total": 1387.0 + }, + { + "date": "3/09", + "total": 1390.0 + }, + { + "date": "3/10", + "total": 1489.0 + }, + { + "date": "3/11", + "total": 1451.0 + }, + { + "date": "3/12", + "total": 1454.0 + }, + { + "date": "3/13", + "total": 425.0 + } + ] + }, + "ordinate_axis_min": "-1800.0", + "ordinate_axis_max": "3700.0", + "goal": "1500" +} diff --git a/tests/test_client.py b/tests/test_client.py index 2b7a693..4582d0b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,11 +1,12 @@ import copy import datetime from collections import OrderedDict -from unittest.mock import patch +from http.cookiejar import CookieJar +from unittest.mock import DEFAULT, patch from measurement.measures import Energy, Weight -from myfitnesspal import Client +import myfitnesspal from .base import MFPTestCase @@ -14,11 +15,16 @@ class TestClient(MFPTestCase): def setUp(self): self.arbitrary_username = "alpha" self.arbitrary_password = "beta" - self.arbitrary_date1 = datetime.date(2015, 4, 20) - self.arbitrary_date2 = datetime.date(2015, 4, 28) - self.client = Client( - self.arbitrary_username, self.arbitrary_password, login=False - ) + self.arbitrary_date1 = datetime.date(2022, 1, 10) + self.arbitrary_date2 = datetime.date(2022, 1, 9) + + with patch.multiple( + "myfitnesspal.Client", _get_auth_data=DEFAULT, _get_user_metadata=DEFAULT + ) as patches: + patches["_get_user_metadata"].return_value = {"username": ""} + + self.client = myfitnesspal.Client(cookiejar=CookieJar()) + super().setUp() def test_get_measurement_ids(self): @@ -26,13 +32,10 @@ def test_get_measurement_ids(self): actual_ids = self.client._get_measurement_ids(document) expected_ids = { - "Weight": 1, - "Body Fat": 91955886, - "Butt": 92738807, - "Bicep": 92738811, - "Quad": 92738815, - "Mid Section": 92738819, - "Shoulders": 92738861, + "Hips": "278869596622717", + "Neck": "278869604978685", + "Waist": "278319840808829", + "Weight": "", } self.assertEqual( @@ -52,23 +55,15 @@ def test_get_meals(self): def test_get_measurements(self): with patch.object(self.client, "_get_document_for_url") as get_doc: get_doc.return_value = self.get_html_document("measurements.html") + get_doc.called actual_measurements = self.client.get_measurements( - "Body Fat", + "Weight", self.arbitrary_date1, self.arbitrary_date2, ) expected_measurements = OrderedDict( - [ - (datetime.date(2015, 4, 28), 19.2), - (datetime.date(2015, 4, 27), 19.2), - (datetime.date(2015, 4, 26), 19.0), - (datetime.date(2015, 4, 25), 18.7), - (datetime.date(2015, 4, 23), 18.7), - (datetime.date(2015, 4, 22), 18.4), - (datetime.date(2015, 4, 21), 18.9), - (datetime.date(2015, 4, 20), 19.1), - ] + [(datetime.date(2022, 1, 10), 155.0), (datetime.date(2022, 1, 9), 156.0)] ) self.assertEqual( @@ -431,3 +426,32 @@ def test_get_completed_day(self): day.complete, True, ) + + def test_get_report(self): + with patch.object(self.client, "_get_json_for_url") as get_doc: + get_doc.return_value = self.get_json_data( + "report_nutrition_net_calories.json" + ) + actual_measurements = self.client.get_report( + report_name="Net Calories", + report_category="Nutrition", + lower_bound=datetime.date.today() - datetime.timedelta(days=4), + ) + + # Dates are determined based on the assumption that the results will + # always start from the current day. Dates held in sample data file are + # therefore irrelevant for this test. + + expected_measurements = OrderedDict( + sorted( + [ + (datetime.date.today(), 425.0), + (datetime.date.today() - datetime.timedelta(days=1), 1454.0), + (datetime.date.today() - datetime.timedelta(days=2), 1451.0), + (datetime.date.today() - datetime.timedelta(days=3), 1489.0), + (datetime.date.today() - datetime.timedelta(days=4), 1390.0), + ] + ) + ) + + self.assertEqual(expected_measurements, actual_measurements)