mattdodge / yahoofantasy

A Python SDK for the Yahoo! Fantasy Sports API

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Way to get all years league ID for a League

joeyagreco opened this issue · comments

Currently, you can get all fantasy leagues for a given year using the get_leagues() method.

Having a way to see the previous/next year's league ID would be useful in getting all years for a single league.

Example:

A league has years 2018, 2019, 2020.

A user wants to get stats on all of those years, but only knows the ending League ID (the ID for 2020).

There should be a way to get the previous year's league ID (in this case, the league ID for 2019) from the league that is fetched for 2020.

This is a good request with an unfortunate amount of difficulty I'm afraid.

First off, you can get leagues from past seasons by doing something like this

leagues_2021 = ctx.get_leagues('nfl', 2021)
leagues_2020 = ctx.get_leagues('nfl', 2020)

That's going to give you two lists of leagues, but unfortunately there is no "link" between the leagues across seasons. As a human, it wouldn't be too hard to look through the list of your leagues for each season and figure out which one maps to what, but I assume this issue is directed more towards a programmatic approach.

The fundamental problem is that Yahoo doesn't expose the link between leagues across seasons in their API. In other words, nowhere in the response body of a league does it show that league's ID from a previous year. At least not that I can find.

It's also worth pointing out that a real-world, multi-season fantasy league doesn't always appear that way in Yahoo, depending on how the league was launched or how the commissioner configures it. Commissioners can configure "league history" by linking past-year's leagues in Commissioner Tools - this is what enables the season selection dropdown from the main league page. Unfortunately, this information is not made available in the API. If a commissioner has not done this and the leagues were not renewed a certain way that dropdown won't appear.

I think the best approach here may be to make a guess at past leagues based on the managers in the league. Assume most of the managers across seasons are the same. This obviously isn't foolproof, but it can at least get close. Here's a code-snippet that should be close:

def guess_past_league(league, game, season):
  existing_managers = set([t.manager.guid for t in league.teams()])
  for target_league in ctx.get_leagues(game, season):
    target_managers = set([t.manager.guid for t in target_league.teams()])
    # consider it a match if 70% of the managers were in this league too
    if len(target_managers & existing_managers) >= (len(existing_managers) * 0.7):
      break
  else:
    return None
  return target_league

Now, with a given league from 2021, you can call the method like so to get the guess at the 2020 league

league_2020 = guess_past_league(league_2021, 'nfl', 2020)

I ran this through a few of my leagues and it works. There are certainly edge cases but I can't really think of a better approach until Yahoo makes this available (don't hold your breath). If this looks good to you and addresses your question @joeyagreco I can toss this into the library as a util function and clean it up a bit, I can see this being useful to others as well.

Thank you for the thorough response.

I spent some time looking through the API response myself as well and was also unable to find any sort of link. I was hoping I had missed something.

I believe your solution is the best way to approach this until/if Yahoo ever makes this information available.

The league endpoint actually does have a field named "renew" and "renewed". "renew" points to a prior year league, and "renewed" the next year. When I do it from Postman it returns the xml value like, 399_12345. But using the python client it drops the underscore, so I get 39912345. I do have league history configured in all my leagues so not sure if that matters for that or not.
Also, I like the clever thinking with checking the managers in the league, 😃 Though unfortunately picking a somewhat random percentage could be somewhat dicey. I know some cases where it would certainly fail in my case, since sometimes we've had a league with more than 3 new owners out of 10 team league, so would fall below that 70% threshold. Also, in my case it would probably work if the bar was set to 20 or 30%, but I'm sure there would be other leagues where that wouldn't work.

The league endpoint actually does have a field named "renew" and "renewed". "renew" points to a prior year league, and "renewed" the next year. When I do it from Postman it returns the xml value like, 399_12345. But using the python client it drops the underscore, so I get 39912345. I do have league history configured in all my leagues so not sure if that matters for that or not. Also, I like the clever thinking with checking the managers in the league, 😃 Though unfortunately picking a somewhat random percentage could be somewhat dicey. I know some cases where it would certainly fail in my case, since sometimes we've had a league with more than 3 new owners out of 10 team league, so would fall below that 70% threshold. Also, in my case it would probably work if the bar was set to 20 or 30%, but I'm sure there would be other leagues where that wouldn't work.

How would you use the renew/renewed fields (in my league, both are returned but are different) to link leagues?

In your example, would all "linked" leagues have a similarity in the renew/renewed field/s that would allow you to determine if they are linked?

How would you use the renew/renewed fields (in my league, both are returned but are different) to link leagues?

So they should be different, one is telling you the past years id and one is telling the next years id. So if I do a league from 2021 for example, I am looking at the information for 406.l.55555.
And I get these values:
<renew>399_12345</renew => 2020 league
<renewed>414_78901<renewed> => 2022 league

Then if you call end end point for the 399_12345 league (which is 2020), you will get the renew and renewed values for the 2019 and 2021 leagues.

How would you use the renew/renewed fields (in my league, both are returned but are different) to link leagues?

So they should be different, one is telling you the past years id and one is telling the next years id. So if I do a league from 2021 for example, I am looking at the information for 406.l.55555. And I get these values: <renew>399_12345</renew => 2020 league <renewed>414_78901<renewed> => 2022 league

Then if you call end end point for the 399_12345 league (which is 2020), you will get the renew and renewed values for the 2019 and 2021 leagues.

That's great!

So in your example, 399_12345 would translate to what being the 2020 league ID? 39912345? 12345?

Yes 12345 would be the league ID. The 399 is yahoo's "game_key".

Yes 12345 would be the league ID. The 399 is yahoo's "game_key".

Gotcha, so we would need this library to include the underscore in the renew and renewed fields in order to link leagues.

@mattdodge is this something you could implement?

Interesting - I'm not seeing that behavior in one of my multi-year leagues, but I do see it in another. Great find @brian-dev8! I suspect that one of the leagues wasn't actually renewed, but the commissioner linked them in Commissioner Tools. That tells me that this approach will work for renewed leagues but not manually re-created leagues.

Python allows underscores in numbers (the idea being you can make them commas, like 1_000_000 representing 1 million). So the parser is actually thinking that renew field is a number. The easiest way to extract the game ID and the league ID would be something like this

game_id = str(league.renew)[:3]
league_id = str(league.renew)[3:]

This assumes Yahoo game IDs are 3 digits, which they always have been and, if they follow their current incrementing scheme, will be for quite some time. No guarantees on that though.

The alternative, slightly more fool-proof but far more complicated, approach would be to make the API call yourself and then include set_raw = True when calling from_response_object. Relevant function code here and here is the code that makes the league API request.

Good points.

A way to validate would potentially be using league ID length.

If we assume the prefix is always length of 3 and league ID is always length of 6 we can run a check wherever we use this that says if the total length != 9 then assume this way of getting things will not work.

I have some league IDs that are 5 digits, I think it has to do with how early your league was registered, I think some might be even fewer digits. Game codes since 2005 are all fixed at 3 digits, for now at least. They are all loaded here if you want to take a look.

I'm also afraid my other approach with the set_raw won't work either. It looks like this underscore behavior goes all the way down to to the XML parser level. Here is the problematic code and a snippet to demonstrate:

>>> from xmljson import badgerfish as bf
>>> from xml.etree.ElementTree import fromstring
>>> bf.data(fromstring('<test>123_456</test>'))
OrderedDict([('test', OrderedDict([('$', 123456)]))])
>>> >>> type(bf.data(fromstring('<test>123_456</test>'))['test']['$'])
<class 'int'>

I'm afraid this won't be feasible to fix unless we switch the XML parser or the maintainer of that library addresses this, but that library looks a bit stale so I'm not too optimistic for that.