Module 3 - Day 3

Login to Lab using your credentials. There is a notebook with name 3-3.ipynb already created for you. Open that and use it for today’s training.

Shut down all previous notebooks.

You can access live notes from https://live.arcesium-lab.pipal.in

Web Scraping

requests is third party library which implements functions to do web request from python. it support all four HTTP methods (get, post, put, delete)

pip install requests

import requests
url = "https://www.python.org/"
response = requests.get(url)
response # response 200 means everything went well!
<Response [200]>
url_wrong = "https://thereis.nowebsite.like.this/"
response = requests.get(url_wrong)
---------------------------------------------------------------------------
gaierror                                  Traceback (most recent call last)
File /opt/tljh/user/lib/python3.10/site-packages/urllib3/connection.py:174, in HTTPConnection._new_conn(self)
    173 try:
--> 174     conn = connection.create_connection(
    175         (self._dns_host, self.port), self.timeout, **extra_kw
    176     )
    178 except SocketTimeout:

File /opt/tljh/user/lib/python3.10/site-packages/urllib3/util/connection.py:72, in create_connection(address, timeout, source_address, socket_options)
     68     return six.raise_from(
     69         LocationParseError(u"'%s', label empty or too long" % host), None
     70     )
---> 72 for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
     73     af, socktype, proto, canonname, sa = res

File /opt/tljh/user/lib/python3.10/socket.py:955, in getaddrinfo(host, port, family, type, proto, flags)
    954 addrlist = []
--> 955 for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
    956     af, socktype, proto, canonname, sa = res

gaierror: [Errno -2] Name or service not known

During handling of the above exception, another exception occurred:

NewConnectionError                        Traceback (most recent call last)
File /opt/tljh/user/lib/python3.10/site-packages/urllib3/connectionpool.py:703, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    702 # Make the request on the httplib connection object.
--> 703 httplib_response = self._make_request(
    704     conn,
    705     method,
    706     url,
    707     timeout=timeout_obj,
    708     body=body,
    709     headers=headers,
    710     chunked=chunked,
    711 )
    713 # If we're going to release the connection in ``finally:``, then
    714 # the response doesn't need to know about the connection. Otherwise
    715 # it will also try to release it and we'll have a double-release
    716 # mess.

File /opt/tljh/user/lib/python3.10/site-packages/urllib3/connectionpool.py:386, in HTTPConnectionPool._make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    385 try:
--> 386     self._validate_conn(conn)
    387 except (SocketTimeout, BaseSSLError) as e:
    388     # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout.

File /opt/tljh/user/lib/python3.10/site-packages/urllib3/connectionpool.py:1042, in HTTPSConnectionPool._validate_conn(self, conn)
   1041 if not getattr(conn, "sock", None):  # AppEngine might not have  `.sock`
-> 1042     conn.connect()
   1044 if not conn.is_verified:

File /opt/tljh/user/lib/python3.10/site-packages/urllib3/connection.py:363, in HTTPSConnection.connect(self)
    361 def connect(self):
    362     # Add certificate verification
--> 363     self.sock = conn = self._new_conn()
    364     hostname = self.host

File /opt/tljh/user/lib/python3.10/site-packages/urllib3/connection.py:186, in HTTPConnection._new_conn(self)
    185 except SocketError as e:
--> 186     raise NewConnectionError(
    187         self, "Failed to establish a new connection: %s" % e
    188     )
    190 return conn

NewConnectionError: <urllib3.connection.HTTPSConnection object at 0x7365c5c98760>: Failed to establish a new connection: [Errno -2] Name or service not known

During handling of the above exception, another exception occurred:

MaxRetryError                             Traceback (most recent call last)
File /opt/tljh/user/lib/python3.10/site-packages/requests/adapters.py:667, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    666 try:
--> 667     resp = conn.urlopen(
    668         method=request.method,
    669         url=url,
    670         body=request.body,
    671         headers=request.headers,
    672         redirect=False,
    673         assert_same_host=False,
    674         preload_content=False,
    675         decode_content=False,
    676         retries=self.max_retries,
    677         timeout=timeout,
    678         chunked=chunked,
    679     )
    681 except (ProtocolError, OSError) as err:

File /opt/tljh/user/lib/python3.10/site-packages/urllib3/connectionpool.py:787, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    785     e = ProtocolError("Connection aborted.", e)
--> 787 retries = retries.increment(
    788     method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]
    789 )
    790 retries.sleep()

File /opt/tljh/user/lib/python3.10/site-packages/urllib3/util/retry.py:592, in Retry.increment(self, method, url, response, error, _pool, _stacktrace)
    591 if new_retry.is_exhausted():
--> 592     raise MaxRetryError(_pool, url, error or ResponseError(cause))
    594 log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)

MaxRetryError: HTTPSConnectionPool(host='thereis.nowebsite.like.this', port=443): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7365c5c98760>: Failed to establish a new connection: [Errno -2] Name or service not known'))

During handling of the above exception, another exception occurred:

ConnectionError                           Traceback (most recent call last)
Cell In[5], line 2
      1 url_wrong = "https://thereis.nowebsite.like.this/"
----> 2 response = requests.get(url_wrong)

File /opt/tljh/user/lib/python3.10/site-packages/requests/api.py:73, in get(url, params, **kwargs)
     62 def get(url, params=None, **kwargs):
     63     r"""Sends a GET request.
     64 
     65     :param url: URL for the new :class:`Request` object.
   (...)
     70     :rtype: requests.Response
     71     """
---> 73     return request("get", url, params=params, **kwargs)

File /opt/tljh/user/lib/python3.10/site-packages/requests/api.py:59, in request(method, url, **kwargs)
     55 # By using the 'with' statement we are sure the session is closed, thus we
     56 # avoid leaving sockets open which can trigger a ResourceWarning in some
     57 # cases, and look like a memory leak in others.
     58 with sessions.Session() as session:
---> 59     return session.request(method=method, url=url, **kwargs)

File /opt/tljh/user/lib/python3.10/site-packages/requests/sessions.py:589, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    584 send_kwargs = {
    585     "timeout": timeout,
    586     "allow_redirects": allow_redirects,
    587 }
    588 send_kwargs.update(settings)
--> 589 resp = self.send(prep, **send_kwargs)
    591 return resp

File /opt/tljh/user/lib/python3.10/site-packages/requests/sessions.py:703, in Session.send(self, request, **kwargs)
    700 start = preferred_clock()
    702 # Send the request
--> 703 r = adapter.send(request, **kwargs)
    705 # Total elapsed time of the request (approximately)
    706 elapsed = preferred_clock() - start

File /opt/tljh/user/lib/python3.10/site-packages/requests/adapters.py:700, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    696     if isinstance(e.reason, _SSLError):
    697         # This branch is for urllib3 v1.22 and later.
    698         raise SSLError(e, request=request)
--> 700     raise ConnectionError(e, request=request)
    702 except ClosedPoolError as e:
    703     raise ConnectionError(e, request=request)

ConnectionError: HTTPSConnectionPool(host='thereis.nowebsite.like.this', port=443): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7365c5c98760>: Failed to establish a new connection: [Errno -2] Name or service not known'))
url = "https://www.python.org/"
response = requests.get(url)
response
<Response [200]>
response.status_code
200
print(response.text[:800]) # if website retuns any text or html data
<!doctype html>
<!--[if lt IE 7]>   <html class="no-js ie6 lt-ie7 lt-ie8 lt-ie9">   <![endif]-->
<!--[if IE 7]>      <html class="no-js ie7 lt-ie8 lt-ie9">          <![endif]-->
<!--[if IE 8]>      <html class="no-js ie8 lt-ie9">                 <![endif]-->
<!--[if gt IE 8]><!--><html class="no-js" lang="en" dir="ltr">  <!--<![endif]-->

<head>
    <!-- Google tag (gtag.js) -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=G-TF35YF9CVH"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', 'G-TF35YF9CVH');
    </script>
    <!-- Plausible.io analytics -->
    <script defer data-domain="python.org" src="https://plausible.io/js/script.js"></script>

   
data = response.content
type(data)
bytes
xlurl = "https://github.com/vikipedia/python-trainings/raw/master/online_course/source/module2/wallet.xlsx"
import requests
r = requests.get(xlurl)
r.status_code
200
#r.text # this will not have data of excel in proper format! 
with open("wallet.xlsx", "wb") as f:
    f.write(r.content) # this will have binary data
import os
os.getcwd()
'/home/jupyter-pipal'

problem

  • Write a function download which will download file from internetr and save in as file on local system.

  • Write a function to download_notes all these notes from live notes website and save to your local system There is a patter in live notes url

    Module 1, day1 => https://live.arcesium-lab.pipal.in/1-1.html
    Module 1, day2 => https://live.arcesium-lab.pipal.in/1-2.html
    Module 3, day3 => https://live.arcesium-lab.pipal.in/3-3.html
def geturl(baseurl, module, day):
    return f"{baseurl}/" + getfilename(module, day)

def download(url, filename):
    r = requests.get(url)
    if r.status_code == 200:
        with open(filename, "wb") as f:
            f.write(r.content)
    else:
        print(f"Error in downloading {filename}")


def getfilename(module, day):
    return f"{module}-{day}.html"

def getfilepath(module, day, folder):
    return os.path.join(folder, getfilename(module, day))

def download_notes(baseurl, folder):
    os.makedirs(folder, exist_ok=True)
    for module in range(1, 3):
        for day in range(1, 6):
            url = geturl(baseurl, module, day)
            filename = getfilepath(module, day, folder)
            print(f"Downloading {filename}")
            download(url, filename)
download_notes("https://live.arcesium-lab.pipal.in/", "livenotes")
Downloading livenotes/1-1.html
Downloading livenotes/1-2.html
Downloading livenotes/1-3.html
Downloading livenotes/1-4.html
Downloading livenotes/1-5.html
Downloading livenotes/2-1.html
Downloading livenotes/2-2.html
Downloading livenotes/2-3.html
Downloading livenotes/2-4.html
Downloading livenotes/2-5.html

API

There are some websites which sale data using WEBAPI. it consists of APIURL and documentation of paramaters required to use the API to download data

We will use www.alphavantage.co API

import requests

url = "https://www.alphavantage.co/query"

params = {"function":"TIME_SERIES_INTRADAY",
          "symbol":"IBM",
          "interval": "5min",
          "apikey":"demo"}

r = requests.get(url, params=params)
r
<Response [200]>
data = r.json()
type(data)
dict
data.keys()
dict_keys(['Meta Data', 'Time Series (5min)'])
data['Meta Data']
{'1. Information': 'Intraday (5min) open, high, low, close prices and volume',
 '2. Symbol': 'IBM',
 '3. Last Refreshed': '2024-08-21 19:55:00',
 '4. Interval': '5min',
 '5. Output Size': 'Compact',
 '6. Time Zone': 'US/Eastern'}
import pandas as pd

pd.DataFrame(data['Time Series (5min)'])
2024-08-21 19:55:00 2024-08-21 19:50:00 2024-08-21 19:45:00 2024-08-21 19:40:00 2024-08-21 19:35:00 2024-08-21 19:30:00 2024-08-21 19:25:00 2024-08-21 19:15:00 2024-08-21 19:05:00 2024-08-21 19:00:00 ... 2024-08-21 11:50:00 2024-08-21 11:45:00 2024-08-21 11:40:00 2024-08-21 11:35:00 2024-08-21 11:30:00 2024-08-21 11:25:00 2024-08-21 11:20:00 2024-08-21 11:15:00 2024-08-21 11:10:00 2024-08-21 11:05:00
1. open 197.1900 197.1900 197.1800 197.1100 197.2200 197.1800 197.1800 197.1900 197.2900 197.2100 ... 194.8000 194.9150 194.8100 194.7250 194.7900 194.9640 194.8390 194.8900 194.6300 194.5300
2. high 197.2200 197.1900 197.1800 197.1800 197.2200 197.3000 197.1800 197.1900 197.2900 197.2900 ... 194.8310 194.9600 194.9200 194.8900 194.8800 195.0200 195.0800 195.0600 194.9600 194.7100
3. low 197.1800 197.1900 197.1800 197.1100 197.1000 197.1800 197.1800 196.9400 197.1000 197.2100 ... 194.7100 194.7900 194.7400 194.7020 194.6600 194.8000 194.8390 194.7800 194.6000 194.4700
4. close 197.2200 197.1900 197.1800 197.1800 197.1000 197.3000 197.1800 196.9400 197.1000 197.2900 ... 194.8050 194.8000 194.9150 194.8100 194.7250 194.8250 195.0100 194.8700 194.9500 194.6100
5. volume 12 2 1 13 75 24 25 150 13 387015 ... 25459 26803 23019 14291 20691 17639 12343 17589 13712 12457

5 rows × 100 columns

def get_timeseries_data(ticker):
    url = "https://www.alphavantage.co/query"
    
    params = {"function":"TIME_SERIES_INTRADAY",
              "symbol": ticker,
              "interval": "5min",
              "apikey":"demo"}
    
    r = requests.get(url, params=params)
    timeseries = r.json()['Time Series (5min)']
    return pd.DataFrame(timeseries).transpose()
get_timeseries_data("IBM")
1. open 2. high 3. low 4. close 5. volume
2024-08-21 19:55:00 197.1900 197.2200 197.1800 197.2200 12
2024-08-21 19:50:00 197.1900 197.1900 197.1900 197.1900 2
2024-08-21 19:45:00 197.1800 197.1800 197.1800 197.1800 1
2024-08-21 19:40:00 197.1100 197.1800 197.1100 197.1800 13
2024-08-21 19:35:00 197.2200 197.2200 197.1000 197.1000 75
... ... ... ... ... ...
2024-08-21 11:25:00 194.9640 195.0200 194.8000 194.8250 17639
2024-08-21 11:20:00 194.8390 195.0800 194.8390 195.0100 12343
2024-08-21 11:15:00 194.8900 195.0600 194.7800 194.8700 17589
2024-08-21 11:10:00 194.6300 194.9600 194.6000 194.9500 13712
2024-08-21 11:05:00 194.5300 194.7100 194.4700 194.6100 12457

100 rows × 5 columns

get_timeseries_data("MICROSOFT")
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[37], line 1
----> 1 get_timeseries_data("MICROSOFT")

Cell In[35], line 10, in get_timeseries_data(ticker)
      4 params = {"function":"TIME_SERIES_INTRADAY",
      5           "symbol": ticker,
      6           "interval": "5min",
      7           "apikey":"demo"}
      9 r = requests.get(url, params=params)
---> 10 timeseries = r.json()['Time Series (5min)']
     11 return pd.DataFrame(timeseries).transpose()

KeyError: 'Time Series (5min)'
def get_ticker_name(keyword):
    url = f"https://www.alphavantage.co/query?function=SYMBOL_SEARCH&{keyword}=tesco&apikey=demo"
    r = requests.get(url)
    return r.json()
get_ticker_name("microsoft")
{'Information': 'The **demo** API key is for demo purposes only. Please claim your free API key at (https://www.alphavantage.co/support/#api-key) to explore our full API offerings. It takes fewer than 20 seconds.'}
def get_ticker_name(keyword, apikey="demo"):
    url = f"https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords={keyword}&apikey={apikey}"
    r = requests.get(url)
    return r.json()
get_ticker_name("APPLE", "UKVFE0JLE0TBPDEF")
{'bestMatches': [{'1. symbol': 'APLE',
   '2. name': 'Apple Hospitality REIT Inc',
   '3. type': 'Equity',
   '4. region': 'United States',
   '5. marketOpen': '09:30',
   '6. marketClose': '16:00',
   '7. timezone': 'UTC-04',
   '8. currency': 'USD',
   '9. matchScore': '0.8889'},
  {'1. symbol': 'AAPL',
   '2. name': 'Apple Inc',
   '3. type': 'Equity',
   '4. region': 'United States',
   '5. marketOpen': '09:30',
   '6. marketClose': '16:00',
   '7. timezone': 'UTC-04',
   '8. currency': 'USD',
   '9. matchScore': '0.7143'},
  {'1. symbol': 'AAPL34.SAO',
   '2. name': 'Apple Inc',
   '3. type': 'Equity',
   '4. region': 'Brazil/Sao Paolo',
   '5. marketOpen': '10:00',
   '6. marketClose': '17:30',
   '7. timezone': 'UTC-03',
   '8. currency': 'BRL',
   '9. matchScore': '0.7143'},
  {'1. symbol': 'APC.DEX',
   '2. name': 'Apple Inc',
   '3. type': 'Equity',
   '4. region': 'XETRA',
   '5. marketOpen': '08:00',
   '6. marketClose': '20:00',
   '7. timezone': 'UTC+02',
   '8. currency': 'EUR',
   '9. matchScore': '0.7143'},
  {'1. symbol': 'APC.FRK',
   '2. name': 'Apple Inc',
   '3. type': 'Equity',
   '4. region': 'Frankfurt',
   '5. marketOpen': '08:00',
   '6. marketClose': '20:00',
   '7. timezone': 'UTC+02',
   '8. currency': 'EUR',
   '9. matchScore': '0.7143'},
  {'1. symbol': 'AGPL',
   '2. name': 'Apple Green Holding Inc',
   '3. type': 'Equity',
   '4. region': 'United States',
   '5. marketOpen': '09:30',
   '6. marketClose': '16:00',
   '7. timezone': 'UTC-04',
   '8. currency': 'USD',
   '9. matchScore': '0.6667'},
  {'1. symbol': '0R2V.LON',
   '2. name': 'Apple Inc.',
   '3. type': 'Equity',
   '4. region': 'United Kingdom',
   '5. marketOpen': '08:00',
   '6. marketClose': '16:30',
   '7. timezone': 'UTC+01',
   '8. currency': 'USD',
   '9. matchScore': '0.6667'},
  {'1. symbol': '500014.BSE',
   '2. name': 'Apple Finance Limited',
   '3. type': 'Equity',
   '4. region': 'India/Bombay',
   '5. marketOpen': '09:15',
   '6. marketClose': '15:30',
   '7. timezone': 'UTC+5.5',
   '8. currency': 'INR',
   '9. matchScore': '0.3846'},
  {'1. symbol': '48T.FRK',
   '2. name': 'APPLE HOSPITALITY REIT',
   '3. type': 'Equity',
   '4. region': 'Frankfurt',
   '5. marketOpen': '08:00',
   '6. marketClose': '20:00',
   '7. timezone': 'UTC+02',
   '8. currency': 'EUR',
   '9. matchScore': '0.3704'},
  {'1. symbol': '603020.SHH',
   '2. name': 'Apple Flavor Fragrance Group Company Ltd',
   '3. type': 'Equity',
   '4. region': 'Shanghai',
   '5. marketOpen': '09:30',
   '6. marketClose': '15:00',
   '7. timezone': 'UTC+08',
   '8. currency': 'CNY',
   '9. matchScore': '0.2222'}]}
def get_ticker_name(keyword, apikey="demo"):
    url = f"https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords={keyword}&apikey={apikey}"
    r = requests.get(url)
    return [ match['1. symbol'] for match in r.json()['bestMatches']]
get_ticker_name("APPLE", "UKVFE0JLE0TBPDEF")
['APLE',
 'AAPL',
 'AAPL34.SAO',
 'APC.DEX',
 'APC.FRK',
 'AGPL',
 '0R2V.LON',
 '500014.BSE',
 '48T.FRK',
 '603020.SHH']
%load_problem fibs-api
Problem: Fibs API

Write a function fibs to generate fibonacci numbers using fibs API at https://numbers.apps.pipal.in/.

The API takes 3 query parameters a, b and n and generates n fibonacci numbers starting with a and b.

$ curl 'https://numbers.apps.pipal.in/fibs?a=3&b=4&n=10'
3
4
7
11
18
29
47
76
123
199

Write a function fibs that takes three numbers a, b and n as arguments and returns a list of fibonacci numbers given by the API.

>>> fibs(3, 4, 10)
[3, 4, 7, 11, 18, 29, 47, 76, 123, 199]

>>> sum(fibs(3, 4, 10))
517

You can verify your solution using:

%verify_problem fibs-api

# your code here

def fibs(a, b, n):
    url = f'https://numbers.apps.pipal.in/fibs?a={a}&b={b}&n={n}'
    r = requests.get(url)
    return r.text.strip().split("\n")
fibs(1, 1, 10)
['1', '1', '2', '3', '5', '8', '13', '21', '34', '55']

def fibs(a, b, n):
    url = f'https://numbers.apps.pipal.in/fibs?a={a}&b={b}&n={n}'
    r = requests.get(url)
    return [int(i) for i in r.text.strip().split("\n")]
fibs(1, 1, 10)
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
%verify_problem fibs-api
Found 7 checks
✓ fibs(3, 4, 10)
✓ sum(fibs(3, 4, 10))
✗ fibs(3, 4, 0)
Traceback (most recent call last):
  File "/opt/sigma/src/sigma/problems.py", line 262, in run
    result = self.do_eval(self.code, env)
  File "/opt/sigma/src/sigma/problems.py", line 242, in do_eval
    return eval(self.code, env)
  File "<string>", line 1, in <module>
  File "/tmp/ipykernel_1369338/2104082634.py", line 4, in fibs
    return [int(i) for i in r.text.strip().split("\n")]
  File "/tmp/ipykernel_1369338/2104082634.py", line 4, in <listcomp>
    return [int(i) for i in r.text.strip().split("\n")]
ValueError: invalid literal for int() with base 10: ''
✓ fibs(3, 4, 1)
✓ fibs(3, 4, 2)
✓ fibs(3, 4, 3)
✓ fibs(1, 1, 6)
💥 Oops! Your solution to problem fibs-api is incorrect or incomplete.
fibs(3,4 , 0)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[59], line 1
----> 1 fibs(3,4 , 0)

Cell In[56], line 4, in fibs(a, b, n)
      2 url = f'https://numbers.apps.pipal.in/fibs?a={a}&b={b}&n={n}'
      3 r = requests.get(url)
----> 4 return [int(i) for i in r.text.strip().split("\n")]

Cell In[56], line 4, in <listcomp>(.0)
      2 url = f'https://numbers.apps.pipal.in/fibs?a={a}&b={b}&n={n}'
      3 r = requests.get(url)
----> 4 return [int(i) for i in r.text.strip().split("\n")]

ValueError: invalid literal for int() with base 10: ''

Authentication

Simple authentication is done using username and password

%%file passwd.txt
vikrant=secret
Writing passwd.txt
def get_auth_details(filepath):
    with open(filepath) as f:
        return f.read().strip().split("=")
username, password = get_auth_details("passwd.txt")
#print(username, password)
requests.get(url, auth=(username, password))
vikrant secret

Kerberos authentication

pip install requests requests-kerberos
from request_kerberos import HTTPKerberosAuth, OPTIONAL
kerberos_auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
r = requests.get(url, auth= kerberos_auth)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[63], line 1
----> 1 from request_kerberos import HTTPKerberosAuth, OPTIONAL
      2 kerberos_auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
      3 r = requests.get(url, auth= kerberos_auth)

ModuleNotFoundError: No module named 'request_kerberos'