We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Scraper error "TypeError: string indices must be integers"  - Yahoo decrypt fail
## Updates
### 2023 January 13
By the time of posting the issue (2023 January 12), the issue only occured sometimes. The library is now (2023 January 13) completely broken and I am unable to retrieve any stock informatio
### 2023 January 14
Fix has been merged to the branch `dev`

## Info about your system:
yfinance version: 0.2.3
Operating system: macOS Monteray 12.0.1
### Snippet that can recreate the error
```
stock = yf.Ticker("^GSPC")
info = stock.info
```
## Error
Message:`TypeError: string indices must be integers`
It seems to be a problem where the scraper is not scraping the correct information, leading to a crash.
### Traceback:
```
Traceback (most recent call last):
  File "/home/2022/szhang139/.local/lib/python3.10/site-packages/apscheduler/executors/base_py3.py", line 30, in run_coroutine_job
    retval = await job.func(*job.args, **job.kwargs)
  File "/home/2022/szhang139/repos/STONK/src/main.py", line 61, in notify
    market = get_major_index(f'Market Close - {daytime.today_date()}')
  File "/home/2022/szhang139/repos/STONK/src/market_info.py", line 63, in get_major_index
    sp500 = get_stock('^GSPC')
  File "/home/2022/szhang139/repos/STONK/src/market_info.py", line 41, in get_stock
    stock_info = get_stock_info(stock_name)
  File "/home/2022/szhang139/repos/STONK/src/market_info.py", line 8, in get_stock_info
    info = stock.info
  File "/home/2022/szhang139/.local/lib/python3.10/site-packages/yfinance/ticker.py", line 138, in info
    return self.get_info()
  File "/home/2022/szhang139/.local/lib/python3.10/site-packages/yfinance/base.py", line 894, in get_info
    data = self._quote.info
  File "/home/2022/szhang139/.local/lib/python3.10/site-packages/yfinance/scrapers/quote.py", line 27, in info
    self._scrape(self.proxy)
  File "/home/2022/szhang139/.local/lib/python3.10/site-packages/yfinance/scrapers/quote.py", line 58, in _scrape
    quote_summary_store = json_data['QuoteSummaryStore']
```
### Frequency
The error occurs in no apparent pattern. Every time it occurs, it seem to persist for some range of time before it recovers back to normal.
n. 


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `yfinance/data.py`
Content:
```
1 import functools
2 from functools import lru_cache
3 
4 import hashlib
5 from base64 import b64decode
6 usePycryptodome = False  # slightly faster
7 # usePycryptodome = True
8 if usePycryptodome:
9     from Crypto.Cipher import AES
10     from Crypto.Util.Padding import unpad
11 else:
12     from cryptography.hazmat.primitives import padding
13     from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
14 
15 import requests as requests
16 import re
17 
18 from frozendict import frozendict
19 
20 try:
21     import ujson as json
22 except ImportError:
23     import json as json
24 
25 cache_maxsize = 64
26 
27 
28 def lru_cache_freezeargs(func):
29     """
30     Decorator transforms mutable dictionary and list arguments into immutable types
31     Needed so lru_cache can cache method calls what has dict or list arguments.
32     """
33 
34     @functools.wraps(func)
35     def wrapped(*args, **kwargs):
36         args = tuple([frozendict(arg) if isinstance(arg, dict) else arg for arg in args])
37         kwargs = {k: frozendict(v) if isinstance(v, dict) else v for k, v in kwargs.items()}
38         args = tuple([tuple(arg) if isinstance(arg, list) else arg for arg in args])
39         kwargs = {k: tuple(v) if isinstance(v, list) else v for k, v in kwargs.items()}
40         return func(*args, **kwargs)
41 
42     # copy over the lru_cache extra methods to this wrapper to be able to access them
43     # after this decorator has been applied
44     wrapped.cache_info = func.cache_info
45     wrapped.cache_clear = func.cache_clear
46     return wrapped
47 
48 
49 def decrypt_cryptojs_aes(data):
50     encrypted_stores = data['context']['dispatcher']['stores']
51     _cs = data["_cs"]
52     _cr = data["_cr"]
53 
54     _cr = b"".join(int.to_bytes(i, length=4, byteorder="big", signed=True) for i in json.loads(_cr)["words"])
55     password = hashlib.pbkdf2_hmac("sha1", _cs.encode("utf8"), _cr, 1, dklen=32).hex()
56 
57     encrypted_stores = b64decode(encrypted_stores)
58     assert encrypted_stores[0:8] == b"Salted__"
59     salt = encrypted_stores[8:16]
60     encrypted_stores = encrypted_stores[16:]
61 
62     def EVPKDF(password, salt, keySize=32, ivSize=16, iterations=1, hashAlgorithm="md5") -> tuple:
63         """OpenSSL EVP Key Derivation Function
64         Args:
65             password (Union[str, bytes, bytearray]): Password to generate key from.
66             salt (Union[bytes, bytearray]): Salt to use.
67             keySize (int, optional): Output key length in bytes. Defaults to 32.
68             ivSize (int, optional): Output Initialization Vector (IV) length in bytes. Defaults to 16.
69             iterations (int, optional): Number of iterations to perform. Defaults to 1.
70             hashAlgorithm (str, optional): Hash algorithm to use for the KDF. Defaults to 'md5'.
71         Returns:
72             key, iv: Derived key and Initialization Vector (IV) bytes.
73 
74         Taken from: https://gist.github.com/rafiibrahim8/0cd0f8c46896cafef6486cb1a50a16d3
75         OpenSSL original code: https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c#L78
76         """
77 
78         assert iterations > 0, "Iterations can not be less than 1."
79 
80         if isinstance(password, str):
81             password = password.encode("utf-8")
82 
83         final_length = keySize + ivSize
84         key_iv = b""
85         block = None
86 
87         while len(key_iv) < final_length:
88             hasher = hashlib.new(hashAlgorithm)
89             if block:
90                 hasher.update(block)
91             hasher.update(password)
92             hasher.update(salt)
93             block = hasher.digest()
94             for _ in range(1, iterations):
95                 block = hashlib.new(hashAlgorithm, block).digest()
96             key_iv += block
97 
98         key, iv = key_iv[:keySize], key_iv[keySize:final_length]
99         return key, iv
100 
101     key, iv = EVPKDF(password, salt, keySize=32, ivSize=16, iterations=1, hashAlgorithm="md5")
102 
103     if usePycryptodome:
104         cipher = AES.new(key, AES.MODE_CBC, iv=iv)
105         plaintext = cipher.decrypt(encrypted_stores)
106         plaintext = unpad(plaintext, 16, style="pkcs7")
107     else:
108         cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
109         decryptor = cipher.decryptor()
110         plaintext = decryptor.update(encrypted_stores) + decryptor.finalize()
111         unpadder = padding.PKCS7(128).unpadder()
112         plaintext = unpadder.update(plaintext) + unpadder.finalize()
113         plaintext = plaintext.decode("utf-8")
114 
115     decoded_stores = json.loads(plaintext)
116     return decoded_stores
117 
118 
119 _SCRAPE_URL_ = 'https://finance.yahoo.com/quote'
120 
121 
122 class TickerData:
123     """
124     Have one place to retrieve data from Yahoo API in order to ease caching and speed up operations
125     """
126     user_agent_headers = {
127         'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'}
128 
129     def __init__(self, ticker: str, session=None):
130         self.ticker = ticker
131         self._session = session or requests
132 
133     def get(self, url, user_agent_headers=None, params=None, proxy=None, timeout=30):
134         proxy = self._get_proxy(proxy)
135         response = self._session.get(
136             url=url,
137             params=params,
138             proxies=proxy,
139             timeout=timeout,
140             headers=user_agent_headers or self.user_agent_headers)
141         return response
142 
143     @lru_cache_freezeargs
144     @lru_cache(maxsize=cache_maxsize)
145     def cache_get(self, url, user_agent_headers=None, params=None, proxy=None, timeout=30):
146         return self.get(url, user_agent_headers, params, proxy, timeout)
147 
148     def _get_proxy(self, proxy):
149         # setup proxy in requests format
150         if proxy is not None:
151             if isinstance(proxy, dict) and "https" in proxy:
152                 proxy = proxy["https"]
153             proxy = {"https": proxy}
154         return proxy
155 
156     @lru_cache_freezeargs
157     @lru_cache(maxsize=cache_maxsize)
158     def get_json_data_stores(self, sub_page: str = None, proxy=None) -> dict:
159         '''
160         get_json_data_stores returns a python dictionary of the data stores in yahoo finance web page.
161         '''
162         if sub_page:
163             ticker_url = "{}/{}/{}".format(_SCRAPE_URL_, self.ticker, sub_page)
164         else:
165             ticker_url = "{}/{}".format(_SCRAPE_URL_, self.ticker)
166 
167         html = self.get(url=ticker_url, proxy=proxy).text
168 
169         # The actual json-data for stores is in a javascript assignment in the webpage
170         try:
171             json_str = html.split('root.App.main =')[1].split(
172                 '(this)')[0].split(';\n}')[0].strip()
173         except IndexError:
174             # Fetch failed, probably because Yahoo spam triggered
175             return {}
176 
177         data = json.loads(json_str)
178 
179         if "_cs" in data and "_cr" in data:
180             data = decrypt_cryptojs_aes(data)
181 
182         if "context" in data and "dispatcher" in data["context"]:
183             # Keep old code, just in case
184             data = data['context']['dispatcher']['stores']
185 
186         # return data
187         new_data = json.dumps(data).replace('{}', 'null')
188         new_data = re.sub(
189             r'{[\'|\"]raw[\'|\"]:(.*?),(.*?)}', r'\1', new_data)
190 
191         return json.loads(new_data)
192
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/yfinance/data.py b/yfinance/data.py
--- a/yfinance/data.py
+++ b/yfinance/data.py
@@ -46,13 +46,33 @@
     return wrapped
 
 
-def decrypt_cryptojs_aes(data):
+def decrypt_cryptojs_aes_stores(data):
     encrypted_stores = data['context']['dispatcher']['stores']
-    _cs = data["_cs"]
-    _cr = data["_cr"]
 
-    _cr = b"".join(int.to_bytes(i, length=4, byteorder="big", signed=True) for i in json.loads(_cr)["words"])
-    password = hashlib.pbkdf2_hmac("sha1", _cs.encode("utf8"), _cr, 1, dklen=32).hex()
+    if "_cs" in data and "_cr" in data:
+        _cs = data["_cs"]
+        _cr = data["_cr"]
+        _cr = b"".join(int.to_bytes(i, length=4, byteorder="big", signed=True) for i in json.loads(_cr)["words"])
+        password = hashlib.pbkdf2_hmac("sha1", _cs.encode("utf8"), _cr, 1, dklen=32).hex()
+    else:
+        # Currently assume one extra key in dict, which is password. Print error if 
+        # more extra keys detected.
+        new_keys = [k for k in data.keys() if k not in ["context", "plugins"]]
+        l = len(new_keys)
+        if l == 0:
+            return None
+        elif l == 1 and isinstance(data[new_keys[0]], str):
+            password_key = new_keys[0]
+        else:
+            msg = "Yahoo has again changed data format, yfinance now unsure which key(s) is for decryption:"
+            k = new_keys[0]
+            k_str = k if len(k) < 32 else k[:32-3]+"..."
+            msg += f" '{k_str}'->{type(data[k])}"
+            for i in range(1, len(new_keys)):
+                msg += f" , '{k_str}'->{type(data[k])}"
+            raise Exception(msg)
+        password_key = new_keys[0]
+        password = data[password_key]
 
     encrypted_stores = b64decode(encrypted_stores)
     assert encrypted_stores[0:8] == b"Salted__"
@@ -98,7 +118,10 @@
         key, iv = key_iv[:keySize], key_iv[keySize:final_length]
         return key, iv
 
-    key, iv = EVPKDF(password, salt, keySize=32, ivSize=16, iterations=1, hashAlgorithm="md5")
+    try:
+        key, iv = EVPKDF(password, salt, keySize=32, ivSize=16, iterations=1, hashAlgorithm="md5")
+    except:
+        raise Exception("yfinance failed to decrypt Yahoo data response")
 
     if usePycryptodome:
         cipher = AES.new(key, AES.MODE_CBC, iv=iv)
@@ -176,15 +199,16 @@
 
         data = json.loads(json_str)
 
-        if "_cs" in data and "_cr" in data:
-            data = decrypt_cryptojs_aes(data)
-
-        if "context" in data and "dispatcher" in data["context"]:
-            # Keep old code, just in case
-            data = data['context']['dispatcher']['stores']
+        stores = decrypt_cryptojs_aes_stores(data)
+        if stores is None:
+            # Maybe Yahoo returned old format, not encrypted
+            if "context" in data and "dispatcher" in data["context"]:
+                stores = data['context']['dispatcher']['stores']
+        if stores is None:
+            raise Exception(f"{self.ticker}: Failed to extract data stores from web request")
 
         # return data
-        new_data = json.dumps(data).replace('{}', 'null')
+        new_data = json.dumps(stores).replace('{}', 'null')
         new_data = re.sub(
             r'{[\'|\"]raw[\'|\"]:(.*?),(.*?)}', r'\1', new_data)

{"golden_diff": "diff --git a/yfinance/data.py b/yfinance/data.py\n--- a/yfinance/data.py\n+++ b/yfinance/data.py\n@@ -46,13 +46,33 @@\n     return wrapped\n \n \n-def decrypt_cryptojs_aes(data):\n+def decrypt_cryptojs_aes_stores(data):\n     encrypted_stores = data['context']['dispatcher']['stores']\n-    _cs = data[\"_cs\"]\n-    _cr = data[\"_cr\"]\n \n-    _cr = b\"\".join(int.to_bytes(i, length=4, byteorder=\"big\", signed=True) for i in json.loads(_cr)[\"words\"])\n-    password = hashlib.pbkdf2_hmac(\"sha1\", _cs.encode(\"utf8\"), _cr, 1, dklen=32).hex()\n+    if \"_cs\" in data and \"_cr\" in data:\n+        _cs = data[\"_cs\"]\n+        _cr = data[\"_cr\"]\n+        _cr = b\"\".join(int.to_bytes(i, length=4, byteorder=\"big\", signed=True) for i in json.loads(_cr)[\"words\"])\n+        password = hashlib.pbkdf2_hmac(\"sha1\", _cs.encode(\"utf8\"), _cr, 1, dklen=32).hex()\n+    else:\n+        # Currently assume one extra key in dict, which is password. Print error if \n+        # more extra keys detected.\n+        new_keys = [k for k in data.keys() if k not in [\"context\", \"plugins\"]]\n+        l = len(new_keys)\n+        if l == 0:\n+            return None\n+        elif l == 1 and isinstance(data[new_keys[0]], str):\n+            password_key = new_keys[0]\n+        else:\n+            msg = \"Yahoo has again changed data format, yfinance now unsure which key(s) is for decryption:\"\n+            k = new_keys[0]\n+            k_str = k if len(k) < 32 else k[:32-3]+\"...\"\n+            msg += f\" '{k_str}'->{type(data[k])}\"\n+            for i in range(1, len(new_keys)):\n+                msg += f\" , '{k_str}'->{type(data[k])}\"\n+            raise Exception(msg)\n+        password_key = new_keys[0]\n+        password = data[password_key]\n \n     encrypted_stores = b64decode(encrypted_stores)\n     assert encrypted_stores[0:8] == b\"Salted__\"\n@@ -98,7 +118,10 @@\n         key, iv = key_iv[:keySize], key_iv[keySize:final_length]\n         return key, iv\n \n-    key, iv = EVPKDF(password, salt, keySize=32, ivSize=16, iterations=1, hashAlgorithm=\"md5\")\n+    try:\n+        key, iv = EVPKDF(password, salt, keySize=32, ivSize=16, iterations=1, hashAlgorithm=\"md5\")\n+    except:\n+        raise Exception(\"yfinance failed to decrypt Yahoo data response\")\n \n     if usePycryptodome:\n         cipher = AES.new(key, AES.MODE_CBC, iv=iv)\n@@ -176,15 +199,16 @@\n \n         data = json.loads(json_str)\n \n-        if \"_cs\" in data and \"_cr\" in data:\n-            data = decrypt_cryptojs_aes(data)\n-\n-        if \"context\" in data and \"dispatcher\" in data[\"context\"]:\n-            # Keep old code, just in case\n-            data = data['context']['dispatcher']['stores']\n+        stores = decrypt_cryptojs_aes_stores(data)\n+        if stores is None:\n+            # Maybe Yahoo returned old format, not encrypted\n+            if \"context\" in data and \"dispatcher\" in data[\"context\"]:\n+                stores = data['context']['dispatcher']['stores']\n+        if stores is None:\n+            raise Exception(f\"{self.ticker}: Failed to extract data stores from web request\")\n \n         # return data\n-        new_data = json.dumps(data).replace('{}', 'null')\n+        new_data = json.dumps(stores).replace('{}', 'null')\n         new_data = re.sub(\n             r'{[\\'|\\\"]raw[\\'|\\\"]:(.*?),(.*?)}', r'\\1', new_data)\n", "issue": "Scraper error \"TypeError: string indices must be integers\"  - Yahoo decrypt fail\n## Updates\r\n### 2023 January 13\r\nBy the time of posting the issue (2023 January 12), the issue only occured sometimes. The library is now (2023 January 13) completely broken and I am unable to retrieve any stock informatio\r\n### 2023 January 14\r\nFix has been merged to the branch `dev`\r\n\r\n## Info about your system:\r\nyfinance version: 0.2.3\r\nOperating system: macOS Monteray 12.0.1\r\n### Snippet that can recreate the error\r\n```\r\nstock = yf.Ticker(\"^GSPC\")\r\ninfo = stock.info\r\n```\r\n## Error\r\nMessage:`TypeError: string indices must be integers`\r\nIt seems to be a problem where the scraper is not scraping the correct information, leading to a crash.\r\n### Traceback:\r\n```\r\nTraceback (most recent call last):\r\n  File \"/home/2022/szhang139/.local/lib/python3.10/site-packages/apscheduler/executors/base_py3.py\", line 30, in run_coroutine_job\r\n    retval = await job.func(*job.args, **job.kwargs)\r\n  File \"/home/2022/szhang139/repos/STONK/src/main.py\", line 61, in notify\r\n    market = get_major_index(f'Market Close - {daytime.today_date()}')\r\n  File \"/home/2022/szhang139/repos/STONK/src/market_info.py\", line 63, in get_major_index\r\n    sp500 = get_stock('^GSPC')\r\n  File \"/home/2022/szhang139/repos/STONK/src/market_info.py\", line 41, in get_stock\r\n    stock_info = get_stock_info(stock_name)\r\n  File \"/home/2022/szhang139/repos/STONK/src/market_info.py\", line 8, in get_stock_info\r\n    info = stock.info\r\n  File \"/home/2022/szhang139/.local/lib/python3.10/site-packages/yfinance/ticker.py\", line 138, in info\r\n    return self.get_info()\r\n  File \"/home/2022/szhang139/.local/lib/python3.10/site-packages/yfinance/base.py\", line 894, in get_info\r\n    data = self._quote.info\r\n  File \"/home/2022/szhang139/.local/lib/python3.10/site-packages/yfinance/scrapers/quote.py\", line 27, in info\r\n    self._scrape(self.proxy)\r\n  File \"/home/2022/szhang139/.local/lib/python3.10/site-packages/yfinance/scrapers/quote.py\", line 58, in _scrape\r\n    quote_summary_store = json_data['QuoteSummaryStore']\r\n```\r\n### Frequency\r\nThe error occurs in no apparent pattern. Every time it occurs, it seem to persist for some range of time before it recovers back to normal.\r\nn. \r\n\n", "before_files": [{"content": "import functools\nfrom functools import lru_cache\n\nimport hashlib\nfrom base64 import b64decode\nusePycryptodome = False  # slightly faster\n# usePycryptodome = True\nif usePycryptodome:\n    from Crypto.Cipher import AES\n    from Crypto.Util.Padding import unpad\nelse:\n    from cryptography.hazmat.primitives import padding\n    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\n\nimport requests as requests\nimport re\n\nfrom frozendict import frozendict\n\ntry:\n    import ujson as json\nexcept ImportError:\n    import json as json\n\ncache_maxsize = 64\n\n\ndef lru_cache_freezeargs(func):\n    \"\"\"\n    Decorator transforms mutable dictionary and list arguments into immutable types\n    Needed so lru_cache can cache method calls what has dict or list arguments.\n    \"\"\"\n\n    @functools.wraps(func)\n    def wrapped(*args, **kwargs):\n        args = tuple([frozendict(arg) if isinstance(arg, dict) else arg for arg in args])\n        kwargs = {k: frozendict(v) if isinstance(v, dict) else v for k, v in kwargs.items()}\n        args = tuple([tuple(arg) if isinstance(arg, list) else arg for arg in args])\n        kwargs = {k: tuple(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n        return func(*args, **kwargs)\n\n    # copy over the lru_cache extra methods to this wrapper to be able to access them\n    # after this decorator has been applied\n    wrapped.cache_info = func.cache_info\n    wrapped.cache_clear = func.cache_clear\n    return wrapped\n\n\ndef decrypt_cryptojs_aes(data):\n    encrypted_stores = data['context']['dispatcher']['stores']\n    _cs = data[\"_cs\"]\n    _cr = data[\"_cr\"]\n\n    _cr = b\"\".join(int.to_bytes(i, length=4, byteorder=\"big\", signed=True) for i in json.loads(_cr)[\"words\"])\n    password = hashlib.pbkdf2_hmac(\"sha1\", _cs.encode(\"utf8\"), _cr, 1, dklen=32).hex()\n\n    encrypted_stores = b64decode(encrypted_stores)\n    assert encrypted_stores[0:8] == b\"Salted__\"\n    salt = encrypted_stores[8:16]\n    encrypted_stores = encrypted_stores[16:]\n\n    def EVPKDF(password, salt, keySize=32, ivSize=16, iterations=1, hashAlgorithm=\"md5\") -> tuple:\n        \"\"\"OpenSSL EVP Key Derivation Function\n        Args:\n            password (Union[str, bytes, bytearray]): Password to generate key from.\n            salt (Union[bytes, bytearray]): Salt to use.\n            keySize (int, optional): Output key length in bytes. Defaults to 32.\n            ivSize (int, optional): Output Initialization Vector (IV) length in bytes. Defaults to 16.\n            iterations (int, optional): Number of iterations to perform. Defaults to 1.\n            hashAlgorithm (str, optional): Hash algorithm to use for the KDF. Defaults to 'md5'.\n        Returns:\n            key, iv: Derived key and Initialization Vector (IV) bytes.\n\n        Taken from: https://gist.github.com/rafiibrahim8/0cd0f8c46896cafef6486cb1a50a16d3\n        OpenSSL original code: https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c#L78\n        \"\"\"\n\n        assert iterations > 0, \"Iterations can not be less than 1.\"\n\n        if isinstance(password, str):\n            password = password.encode(\"utf-8\")\n\n        final_length = keySize + ivSize\n        key_iv = b\"\"\n        block = None\n\n        while len(key_iv) < final_length:\n            hasher = hashlib.new(hashAlgorithm)\n            if block:\n                hasher.update(block)\n            hasher.update(password)\n            hasher.update(salt)\n            block = hasher.digest()\n            for _ in range(1, iterations):\n                block = hashlib.new(hashAlgorithm, block).digest()\n            key_iv += block\n\n        key, iv = key_iv[:keySize], key_iv[keySize:final_length]\n        return key, iv\n\n    key, iv = EVPKDF(password, salt, keySize=32, ivSize=16, iterations=1, hashAlgorithm=\"md5\")\n\n    if usePycryptodome:\n        cipher = AES.new(key, AES.MODE_CBC, iv=iv)\n        plaintext = cipher.decrypt(encrypted_stores)\n        plaintext = unpad(plaintext, 16, style=\"pkcs7\")\n    else:\n        cipher = Cipher(algorithms.AES(key), modes.CBC(iv))\n        decryptor = cipher.decryptor()\n        plaintext = decryptor.update(encrypted_stores) + decryptor.finalize()\n        unpadder = padding.PKCS7(128).unpadder()\n        plaintext = unpadder.update(plaintext) + unpadder.finalize()\n        plaintext = plaintext.decode(\"utf-8\")\n\n    decoded_stores = json.loads(plaintext)\n    return decoded_stores\n\n\n_SCRAPE_URL_ = 'https://finance.yahoo.com/quote'\n\n\nclass TickerData:\n    \"\"\"\n    Have one place to retrieve data from Yahoo API in order to ease caching and speed up operations\n    \"\"\"\n    user_agent_headers = {\n        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'}\n\n    def __init__(self, ticker: str, session=None):\n        self.ticker = ticker\n        self._session = session or requests\n\n    def get(self, url, user_agent_headers=None, params=None, proxy=None, timeout=30):\n        proxy = self._get_proxy(proxy)\n        response = self._session.get(\n            url=url,\n            params=params,\n            proxies=proxy,\n            timeout=timeout,\n            headers=user_agent_headers or self.user_agent_headers)\n        return response\n\n    @lru_cache_freezeargs\n    @lru_cache(maxsize=cache_maxsize)\n    def cache_get(self, url, user_agent_headers=None, params=None, proxy=None, timeout=30):\n        return self.get(url, user_agent_headers, params, proxy, timeout)\n\n    def _get_proxy(self, proxy):\n        # setup proxy in requests format\n        if proxy is not None:\n            if isinstance(proxy, dict) and \"https\" in proxy:\n                proxy = proxy[\"https\"]\n            proxy = {\"https\": proxy}\n        return proxy\n\n    @lru_cache_freezeargs\n    @lru_cache(maxsize=cache_maxsize)\n    def get_json_data_stores(self, sub_page: str = None, proxy=None) -> dict:\n        '''\n        get_json_data_stores returns a python dictionary of the data stores in yahoo finance web page.\n        '''\n        if sub_page:\n            ticker_url = \"{}/{}/{}\".format(_SCRAPE_URL_, self.ticker, sub_page)\n        else:\n            ticker_url = \"{}/{}\".format(_SCRAPE_URL_, self.ticker)\n\n        html = self.get(url=ticker_url, proxy=proxy).text\n\n        # The actual json-data for stores is in a javascript assignment in the webpage\n        try:\n            json_str = html.split('root.App.main =')[1].split(\n                '(this)')[0].split(';\\n}')[0].strip()\n        except IndexError:\n            # Fetch failed, probably because Yahoo spam triggered\n            return {}\n\n        data = json.loads(json_str)\n\n        if \"_cs\" in data and \"_cr\" in data:\n            data = decrypt_cryptojs_aes(data)\n\n        if \"context\" in data and \"dispatcher\" in data[\"context\"]:\n            # Keep old code, just in case\n            data = data['context']['dispatcher']['stores']\n\n        # return data\n        new_data = json.dumps(data).replace('{}', 'null')\n        new_data = re.sub(\n            r'{[\\'|\\\"]raw[\\'|\\\"]:(.*?),(.*?)}', r'\\1', new_data)\n\n        return json.loads(new_data)\n", "path": "yfinance/data.py"}], "after_files": [{"content": "import functools\nfrom functools import lru_cache\n\nimport hashlib\nfrom base64 import b64decode\nusePycryptodome = False  # slightly faster\n# usePycryptodome = True\nif usePycryptodome:\n    from Crypto.Cipher import AES\n    from Crypto.Util.Padding import unpad\nelse:\n    from cryptography.hazmat.primitives import padding\n    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\n\nimport requests as requests\nimport re\n\nfrom frozendict import frozendict\n\ntry:\n    import ujson as json\nexcept ImportError:\n    import json as json\n\ncache_maxsize = 64\n\n\ndef lru_cache_freezeargs(func):\n    \"\"\"\n    Decorator transforms mutable dictionary and list arguments into immutable types\n    Needed so lru_cache can cache method calls what has dict or list arguments.\n    \"\"\"\n\n    @functools.wraps(func)\n    def wrapped(*args, **kwargs):\n        args = tuple([frozendict(arg) if isinstance(arg, dict) else arg for arg in args])\n        kwargs = {k: frozendict(v) if isinstance(v, dict) else v for k, v in kwargs.items()}\n        args = tuple([tuple(arg) if isinstance(arg, list) else arg for arg in args])\n        kwargs = {k: tuple(v) if isinstance(v, list) else v for k, v in kwargs.items()}\n        return func(*args, **kwargs)\n\n    # copy over the lru_cache extra methods to this wrapper to be able to access them\n    # after this decorator has been applied\n    wrapped.cache_info = func.cache_info\n    wrapped.cache_clear = func.cache_clear\n    return wrapped\n\n\ndef decrypt_cryptojs_aes_stores(data):\n    encrypted_stores = data['context']['dispatcher']['stores']\n\n    if \"_cs\" in data and \"_cr\" in data:\n        _cs = data[\"_cs\"]\n        _cr = data[\"_cr\"]\n        _cr = b\"\".join(int.to_bytes(i, length=4, byteorder=\"big\", signed=True) for i in json.loads(_cr)[\"words\"])\n        password = hashlib.pbkdf2_hmac(\"sha1\", _cs.encode(\"utf8\"), _cr, 1, dklen=32).hex()\n    else:\n        # Currently assume one extra key in dict, which is password. Print error if \n        # more extra keys detected.\n        new_keys = [k for k in data.keys() if k not in [\"context\", \"plugins\"]]\n        l = len(new_keys)\n        if l == 0:\n            return None\n        elif l == 1 and isinstance(data[new_keys[0]], str):\n            password_key = new_keys[0]\n        else:\n            msg = \"Yahoo has again changed data format, yfinance now unsure which key(s) is for decryption:\"\n            k = new_keys[0]\n            k_str = k if len(k) < 32 else k[:32-3]+\"...\"\n            msg += f\" '{k_str}'->{type(data[k])}\"\n            for i in range(1, len(new_keys)):\n                msg += f\" , '{k_str}'->{type(data[k])}\"\n            raise Exception(msg)\n        password_key = new_keys[0]\n        password = data[password_key]\n\n    encrypted_stores = b64decode(encrypted_stores)\n    assert encrypted_stores[0:8] == b\"Salted__\"\n    salt = encrypted_stores[8:16]\n    encrypted_stores = encrypted_stores[16:]\n\n    def EVPKDF(password, salt, keySize=32, ivSize=16, iterations=1, hashAlgorithm=\"md5\") -> tuple:\n        \"\"\"OpenSSL EVP Key Derivation Function\n        Args:\n            password (Union[str, bytes, bytearray]): Password to generate key from.\n            salt (Union[bytes, bytearray]): Salt to use.\n            keySize (int, optional): Output key length in bytes. Defaults to 32.\n            ivSize (int, optional): Output Initialization Vector (IV) length in bytes. Defaults to 16.\n            iterations (int, optional): Number of iterations to perform. Defaults to 1.\n            hashAlgorithm (str, optional): Hash algorithm to use for the KDF. Defaults to 'md5'.\n        Returns:\n            key, iv: Derived key and Initialization Vector (IV) bytes.\n\n        Taken from: https://gist.github.com/rafiibrahim8/0cd0f8c46896cafef6486cb1a50a16d3\n        OpenSSL original code: https://github.com/openssl/openssl/blob/master/crypto/evp/evp_key.c#L78\n        \"\"\"\n\n        assert iterations > 0, \"Iterations can not be less than 1.\"\n\n        if isinstance(password, str):\n            password = password.encode(\"utf-8\")\n\n        final_length = keySize + ivSize\n        key_iv = b\"\"\n        block = None\n\n        while len(key_iv) < final_length:\n            hasher = hashlib.new(hashAlgorithm)\n            if block:\n                hasher.update(block)\n            hasher.update(password)\n            hasher.update(salt)\n            block = hasher.digest()\n            for _ in range(1, iterations):\n                block = hashlib.new(hashAlgorithm, block).digest()\n            key_iv += block\n\n        key, iv = key_iv[:keySize], key_iv[keySize:final_length]\n        return key, iv\n\n    try:\n        key, iv = EVPKDF(password, salt, keySize=32, ivSize=16, iterations=1, hashAlgorithm=\"md5\")\n    except:\n        raise Exception(\"yfinance failed to decrypt Yahoo data response\")\n\n    if usePycryptodome:\n        cipher = AES.new(key, AES.MODE_CBC, iv=iv)\n        plaintext = cipher.decrypt(encrypted_stores)\n        plaintext = unpad(plaintext, 16, style=\"pkcs7\")\n    else:\n        cipher = Cipher(algorithms.AES(key), modes.CBC(iv))\n        decryptor = cipher.decryptor()\n        plaintext = decryptor.update(encrypted_stores) + decryptor.finalize()\n        unpadder = padding.PKCS7(128).unpadder()\n        plaintext = unpadder.update(plaintext) + unpadder.finalize()\n        plaintext = plaintext.decode(\"utf-8\")\n\n    decoded_stores = json.loads(plaintext)\n    return decoded_stores\n\n\n_SCRAPE_URL_ = 'https://finance.yahoo.com/quote'\n\n\nclass TickerData:\n    \"\"\"\n    Have one place to retrieve data from Yahoo API in order to ease caching and speed up operations\n    \"\"\"\n    user_agent_headers = {\n        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'}\n\n    def __init__(self, ticker: str, session=None):\n        self.ticker = ticker\n        self._session = session or requests\n\n    def get(self, url, user_agent_headers=None, params=None, proxy=None, timeout=30):\n        proxy = self._get_proxy(proxy)\n        response = self._session.get(\n            url=url,\n            params=params,\n            proxies=proxy,\n            timeout=timeout,\n            headers=user_agent_headers or self.user_agent_headers)\n        return response\n\n    @lru_cache_freezeargs\n    @lru_cache(maxsize=cache_maxsize)\n    def cache_get(self, url, user_agent_headers=None, params=None, proxy=None, timeout=30):\n        return self.get(url, user_agent_headers, params, proxy, timeout)\n\n    def _get_proxy(self, proxy):\n        # setup proxy in requests format\n        if proxy is not None:\n            if isinstance(proxy, dict) and \"https\" in proxy:\n                proxy = proxy[\"https\"]\n            proxy = {\"https\": proxy}\n        return proxy\n\n    @lru_cache_freezeargs\n    @lru_cache(maxsize=cache_maxsize)\n    def get_json_data_stores(self, sub_page: str = None, proxy=None) -> dict:\n        '''\n        get_json_data_stores returns a python dictionary of the data stores in yahoo finance web page.\n        '''\n        if sub_page:\n            ticker_url = \"{}/{}/{}\".format(_SCRAPE_URL_, self.ticker, sub_page)\n        else:\n            ticker_url = \"{}/{}\".format(_SCRAPE_URL_, self.ticker)\n\n        html = self.get(url=ticker_url, proxy=proxy).text\n\n        # The actual json-data for stores is in a javascript assignment in the webpage\n        try:\n            json_str = html.split('root.App.main =')[1].split(\n                '(this)')[0].split(';\\n}')[0].strip()\n        except IndexError:\n            # Fetch failed, probably because Yahoo spam triggered\n            return {}\n\n        data = json.loads(json_str)\n\n        stores = decrypt_cryptojs_aes_stores(data)\n        if stores is None:\n            # Maybe Yahoo returned old format, not encrypted\n            if \"context\" in data and \"dispatcher\" in data[\"context\"]:\n                stores = data['context']['dispatcher']['stores']\n        if stores is None:\n            raise Exception(f\"{self.ticker}: Failed to extract data stores from web request\")\n\n        # return data\n        new_data = json.dumps(stores).replace('{}', 'null')\n        new_data = re.sub(\n            r'{[\\'|\\\"]raw[\\'|\\\"]:(.*?),(.*?)}', r'\\1', new_data)\n\n        return json.loads(new_data)\n", "path": "yfinance/data.py"}]}