coddingtonbear / python-myfitnesspal

Access your meal tracking data stored in MyFitnessPal programatically

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Client login fails as of 2020-02-02

ccolligan opened this issue · comments

Just wanted to check in on this repo to confirm that it is no longer an active way to access myfitnesspal data. Thanks!

We're at the mercy of undocumented APIs and screenscraping; so things do absolutely break from time to time. Could you elaborate, though, on what kind of "no longer works" you're experiencing by perhaps sharing a description of what function you were trying to perform and a description of the problem you experienced (e.g. maybe a traceback?).

I was just trying to initialize the client with my username and password and got this:

Traceback (most recent call last):
  File "/Users/user/Documents/MyFitnessPal_Analysis/setup.py", line 3, in <module>
    client = myfitnesspal.Client(username, password=password)
  File "/opt/anaconda3/envs/app/lib/python3.8/site-packages/myfitnesspal/client.py", line 61, in __init__
    self._login()
  File "/opt/anaconda3/envs/app/lib/python3.8/site-packages/myfitnesspal/client.py", line 116, in _login
    self._auth_data = self._get_auth_data()
  File "/opt/anaconda3/envs/app/lib/python3.8/site-packages/myfitnesspal/client.py", line 132, in _get_auth_data
    return result.json()
  File "/opt/anaconda3/envs/app/lib/python3.8/site-packages/requests/models.py", line 900, in json
    return complexjson.loads(self.text, **kwargs)
  File "/opt/anaconda3/envs/app/lib/python3.8/json/__init__.py", line 357, in loads
    return _default_decoder.decode(s)
  File "/opt/anaconda3/envs/app/lib/python3.8/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/opt/anaconda3/envs/app/lib/python3.8/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Awesome; thanks for the traceback. It looks like we're failing to login due to an HTTP 406 -- I haven't yet figured out exactly what has changed, though, yet.

Here's an annotated traceback if anybody else happens to have a little time to investigate:

╭───────────────────────────── Traceback (most recent call last) ─────────────────────────────╮
│ /home/acoddington/Documents/Projects/python-myfitnesspal/myfitnesspal/cmdline.py:47 in main │
│                                                                                             │
│   44 │                                                                                      │
│   45 │   try:                                                                               │
│   46 │   │   if args.command[0] in COMMANDS:                                                │
│ ❱ 47 │   │   │   COMMANDS[args.command[0]]["function"](args, *extra)                        │
│   48 │   except Exception:                                                                  │
│   49 │   │   console.print_exception(show_locals=args.traceback_locals)                     │
│   50 │   │   console.print(                                                                 │
│                                                                                             │
│ ╭──────────────────────────────────────── locals ─────────────────────────────────────────╮ │
│ │    args = Namespace(command=['day'], debugger=False, loglevel='INFO',                   │ │
│ │           traceback_locals=True)                                                        │ │
│ │ console = <console width=95 ColorSystem.TRUECOLOR>                                      │ │
│ │   extra = ['redacted']      
│ │  parser = ArgumentParser(prog='myfitnesspal', usage=None, description=None,             │ │
│ │           formatter_class=<class 'argparse.RawDescriptionHelpFormatter'>,               │ │
│ │           conflict_handler='error', add_help=True)                                      │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                             │
│ /home/acoddington/Documents/Projects/python-myfitnesspal/myfitnesspal/commands.py:107 in    │
│ day                                                                                         │
│                                                                                             │
│   104 │   args = parser.parse_args(extra)                                                   │
│   105 │                                                                                     │
│   106 │   password = get_password_from_keyring_or_interactive(args.username)                │
│ ❱ 107 │   client = Client(args.username, password)                                          │
│   108 │   day = client.get_date(args.date)                                                  │
│   109 │                                                                                     │
│   110 │   date_str = args.date.strftime("%Y-%m-%d")                                         │
│                                                                                             │
│ ╭──────────────────────────────────────── locals ─────────────────────────────────────────╮ │
│ │     args = Namespace(date=datetime.date(2021, 2, 2), username='redacted')               │ │
│ │    extra = ('redacted',)                                                                │ │
│ │   kwargs = {}                                                                           │ │
│ │   parser = ArgumentParser(prog='myfitnesspal', usage=None, description=None,            │ │
│ │            formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error',  │ │
│ │            add_help=True)                                                               │ │
│ │ password = 'redacted'                                                                   │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                             │
│ /home/acoddington/Documents/Projects/python-myfitnesspal/myfitnesspal/client.py:61 in       │
│ __init__                                                                                    │
│                                                                                             │
│    58 │   │                                                                                 │
│    59 │   │   self.session = requests.Session()                                             │
│    60 │   │   if login:                                                                     │
│ ❱  61 │   │   │   self._login()                                                             │
│    62 │                                                                                     │
│    63 │   @property                                                                         │
│    64 │   def user_id(self) -> Optional[types.MyfitnesspalUserId]:                          │
│                                                                                             │
│ ╭─────────────────────── locals ────────────────────────╮                                   │
│ │      login = True                                     │                                   │
│ │   password = 'redacted            '                   │                                   │
│ │       self = <MyFitnessPal Client for redacted      > │                                   │
│ │ unit_aware = False                                    │                                   │
│ │   username = 'redacted      '                         │                                   │
│ ╰───────────────────────────────────────────────────────╯                                   │
│                                                                                             │
│ /home/acoddington/Documents/Projects/python-myfitnesspal/myfitnesspal/client.py:116 in      │
│ _login                                                                                      │
│                                                                                             │
│   113 │   │   if "Incorrect username or password" in content:                               │
│   114 │   │   │   raise MyfitnesspalLoginError()                                            │
│   115 │   │                                                                                 │
│ ❱ 116 │   │   self._auth_data = self._get_auth_data()                                       │
│   117 │   │   self._user_metadata = self._get_user_metadata()                               │
│   118 │   │                                                                                 │
│   119 │   │   # authenticity token required for measurement set function.                   │
│                                                                                             │
│ ╭──────────────────────────────────────── locals ─────────────────────────────────────────╮ │
│ │ authenticity_token = 'redacted                                                        … │ │
│ │            content = '<html>\r\n<head><title>406 Not                                    │ │
│ │                      Acceptable</title></head>\r\n<body>\r\n<center><h1>406 '+76        │ │
│ │           document = <Element html at 0x7fecb4e0d220>                                   │ │
│ │          login_url = 'https://www.myfitnesspal.com/account/login'                       │ │
│ │             result = <Response [406]>                                                   │ │
│ │               self = <MyFitnessPal Client for redacted>                                 │ │
│ │         utf8_field = '✓'                                                                │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                             │
│ /home/acoddington/Documents/Projects/python-myfitnesspal/myfitnesspal/client.py:132 in      │
│ _get_auth_data                                                                              │
│                                                                                             │
│   129 │   │   │   │   "status code: {status}".format(status=result.status_code)             │
│   130 │   │   │   )                                                                         │
│   131 │   │                                                                                 │
│ ❱ 132 │   │   return result.json()                                                          │
│   133 │                                                                                     │
│   134 │   def _get_user_metadata(self) -> types.UserMetadata:                               │
│   135 │   │   requested_fields = [                                                          │
│                                                                                             │
│ ╭───────────────────── locals ──────────────────────╮                                       │
│ │ result = <Response [200]>                         │                                       │
│ │   self = <MyFitnessPal Client for redacted>       │                                       │
│ ╰───────────────────────────────────────────────────╯                                       │
│                                                                                             │
│ /home/acoddington/Documents/Projects/python-myfitnesspal/env/lib/python3.8/site-packages/re │
│ quests/models.py:898 in json                                                                │
│                                                                                             │
│   895 │   │   │   │   │   # and the server didn't bother to tell us what codec *was*        │
│   896 │   │   │   │   │   # used.                                                           │
│   897 │   │   │   │   │   pass                                                              │
│ ❱ 898 │   │   return complexjson.loads(self.text, **kwargs)                                 │
│   899 │                                                                                     │
│   900 │   @property                                                                         │
│   901 │   def links(self):                                                                  │
│                                                                                             │
│ ╭───────── locals ──────────╮                                                               │
│ │ kwargs = {}               │                                                               │
│ │   self = <Response [200]> │                                                               │
│ ╰───────────────────────────╯                                                               │
│                                                                                             │
│ /usr/lib/python3.8/json/__init__.py:357 in loads                                            │
│                                                                                             │
│   354 │   if (cls is None and object_hook is None and                                       │
│   355 │   │   │   parse_int is None and parse_float is None and                             │
│   356 │   │   │   parse_constant is None and object_pairs_hook is None and not kw):         │
│ ❱ 357 │   │   return _default_decoder.decode(s)                                             │
│   358 │   if cls is None:                                                                   │
│   359 │   │   cls = JSONDecoder                                                             │
│   360 │   if object_hook is not None:                                                       │
│                                                                                             │
│ ╭──────────────────────────────────────── locals ─────────────────────────────────────────╮ │
│ │               cls = None                                                                │ │
│ │                kw = {}                                                                  │ │
│ │       object_hook = None                                                                │ │
│ │ object_pairs_hook = None                                                                │ │
│ │    parse_constant = None                                                                │ │
│ │       parse_float = None                                                                │ │
│ │         parse_int = None                                                                │ │
│ │                 s = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n   │ │
│ │                     "http://www.w3.'+42345                                              │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                             │
│ /usr/lib/python3.8/json/decoder.py:337 in decode                                            │
│                                                                                             │
│   334 │   │   containing a JSON document).                                                  │
│   335 │   │                                                                                 │
│   336 │   │   """                                                                           │
│ ❱ 337 │   │   obj, end = self.raw_decode(s, idx=_w(s, 0).end())                             │
│   338 │   │   end = _w(s, end).end()                                                        │
│   339 │   │   if end != len(s):                                                             │
│   340 │   │   │   raise JSONDecodeError("Extra data", s, end)                               │
│                                                                                             │
│ ╭──────────────────────────────────────── locals ─────────────────────────────────────────╮ │
│ │   _w = <built-in method match of re.Pattern object at 0x7fecb5bfb870>                   │ │
│ │    s = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n                │ │
│ │        "http://www.w3.'+42345                                                           │ │
│ │ self = <json.decoder.JSONDecoder object at 0x7fecb5c03e50>                              │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                             │
│ /usr/lib/python3.8/json/decoder.py:355 in raw_decode                                        │
│                                                                                             │
│   352 │   │   try:                                                                          │
│   353 │   │   │   obj, end = self.scan_once(s, idx)                                         │
│   354 │   │   except StopIteration as err:                                                  │
│ ❱ 355 │   │   │   raise JSONDecodeError("Expecting value", s, err.value) from None          │
│   356 │   │   return obj, end                                                               │
│                                                                                             │
│ ╭──────────────────────────────────────── locals ─────────────────────────────────────────╮ │
│ │  idx = 0                                                                                │ │
│ │    s = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n                │ │
│ │        "http://www.w3.'+42345                                                           │ │
│ │ self = <json.decoder.JSONDecoder object at 0x7fecb5c03e50>                              │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ │
╰─────────────────────────────────────────────────────────────────────────────────────────────╯
JSONDecodeError: Expecting value: line 1 column 1 (char 0)

I've posted a preliminary fix as 304f13d -- it looks like they're doing some amount of user agent checking. As part of that patch, I've set it to use the latest Chrome user agent for the request.

Assuming that its tests pass, I'll push up a new release.

Fix released as 1.16.2. Hopefully that wasn't the folks at MyFitnessPal actually trying to lock us out, and their changes just happened to break things for this library. We'll see!

Hello, i switched to the version 1.16.2 and i am still getting the same error, please advise, thank you very much for fixing the package

Hi, I'm also still seeing this error after updating to the latest version 1.16.2. When I look at the package code (after updating via pip), it seems not all of the commits made it into the release. For example, changes made in bb34f72 to client.py (with comment: "Set User-Agent for all outgoing requests.") do not show up in the latest release (unless I'm overlooking something).

Also, regarding the line in client.py that throws the error in _get_auth_data: => return result.json(). The result object is an html document, and so the json method is tripping up on it. However, when I go directly to the request url in Chrome (https://www.myfitnesspal.com/user/auth_token?refresh=true), Chrome returns a json object (which seems to be the expected result), so it seems to behave differently than what's happening in the python code.

Thanks for this library, I use it every day. Hopefully this issue can get tracked down without much fuss.

Update: I updated my package manually to be in synch with your latest files, and it works fine now (basically, adding in the user agent checking in client.py solved it, as you mentioned above).

Oh, weird; so I'm not sure what was wrong, but the wheel version of the package was for some unknown reason out-of-date. I've taken some special efforts to clear out some things and have re-uploaded a new package as 1.16.4 just a moment ago, and it does appear to be fine now.

Sorry about that!