Implementing a configuration file reader

Before implementing the reader, we are going to add an enumeration to represent both kinds of authentication flow that Spotify provides us with. Let's go ahead and create a file called auth_method.py in the musicterminal/pytify/auth directory with the following content:

from enum import Enum, auto


class AuthMethod(Enum):
CLIENT_CREDENTIALS = auto()
AUTHORIZATION_CODE = auto()

This will define an enumeration with the CLIENT_CREDENTIALS and AUTHORIZATION_CODE properties. Now. we can use these values in the configuration file. Another thing we need to do is create a file called __init__.py in the musicterminal/pytify/auth directory and import the enumeration that we just created:

from .auth_method import AuthMethod

Now, we can continue and create the functions that will read the configuration for us. Create a file called config.py in the musicterminal/pytify/core directory, and let's start by adding some import statements:

import os
import yaml
from collections import namedtuple

from pytify.auth import AuthMethod

First, we import the os module so we can have access to functions that will help us in building the path where the YAML configuration file is located. We also import the yaml package to read the configuration file and, last but not least, we are importing namedtuple from the collections module. We will go into more detail about what namedtuple does later.

The last thing we import is the AuthMethod enumeration that we just created in the pytify.auth module.

Now, we need a model representing the configuration file, so we create a named tuple called Config, such as:

Config = namedtuple('Config', ['client_id',
'client_secret',
'access_token_url',
'auth_url',
'api_version',
'api_url',
'base_url',
'auth_method', ])

The namedtuple is not a new feature in Python and has been around since version 2.6. namedtuple's are tuple-like objects with a name and with fields accessible by attribute lookup. It is possible to create namedtuple in two different ways; let's start Python REPL and try it out:

>>> from collections import namedtuple
>>> User = namedtuple('User', ['firstname', 'lastname', 'email'])
>>> u = User('Daniel','Furtado', 'myemail@test.com')
User(firstname='Daniel', lastname='Furtado', email='myemail@test.com')
>>>

This construct gets two arguments; the first argument is the name of the namedtuple, and the second is an array of str elements representing every field in the namedtuple. It is also possible to specify the fields of the namedtuple by passing a string with every field name separated by a space, such as:

>>> from collections import namedtuple
>>> User = namedtuple('User', 'firstname lastname email')
>>> u = User('Daniel', 'Furtado', 'myemail@test.com')
>>> print(u)
User(firstname='Daniel', lastname='Furtado', email='myemail@test.com')

The namedtuple constructor also has two keyword-arguments:

Verbose, which, when set to True, displays the definition of the class that defines the namedtuple on the terminal. Behind the scenes, namedtuple's are classes and the verbose keyword argument lets us have a sneak peek at how the namedtuple class is constructed. Let's see this in practice on the REPL:

>>> from collections import namedtuple
>>> User = namedtuple('User', 'firstname lastname email', verbose=True)
from builtins import property as _property, tuple as _tuple
from operator import itemgetter as _itemgetter
from collections import OrderedDict

class User(tuple):
'User(firstname, lastname, email)'

__slots__ = ()

_fields = ('firstname', 'lastname', 'email')

def __new__(_cls, firstname, lastname, email):
'Create new instance of User(firstname, lastname, email)'
return _tuple.__new__(_cls, (firstname, lastname, email))

@classmethod
def _make(cls, iterable, new=tuple.__new__, len=len):
'Make a new User object from a sequence or iterable'
result = new(cls, iterable)
if len(result) != 3:
raise TypeError('Expected 3 arguments, got %d' %
len(result))
return result

def _replace(_self, **kwds):
'Return a new User object replacing specified fields with
new values'
result = _self._make(map(kwds.pop, ('firstname', 'lastname',
'email'), _self))
if kwds:
raise ValueError('Got unexpected field names: %r' %
list(kwds))
return result

def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + '(firstname=%r,
lastname=%r, email=%r)'
% self

def _asdict(self):
'Return a new OrderedDict which maps field names to their
values.'
return OrderedDict(zip(self._fields, self))

def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return tuple(self)

firstname = _property(_itemgetter(0), doc='Alias for field
number 0')

lastname = _property(_itemgetter(1), doc='Alias for field number
1')

email = _property(_itemgetter(2), doc='Alias for field number
2')

The other keyword argument is rename, which will rename every property in the namedtuple that has an incorrect naming, for example:

>>> from collections import namedtuple
>>> User = namedtuple('User', 'firstname lastname email 23445', rename=True)
>>> User._fields
('firstname', 'lastname', 'email', '_3')

As you can see, the field 23445 has been automatically renamed to _3, which is the field position.

To access the namedtuple fields, you can use the same syntax when accessing properties in a class, using the namedtupleUser as shown in the preceding example. If we would like to access the lastname property, we can just write u.lastname.

Now that we have the namedtuple representing our configuration file, it is time to add the function that will perform the work of loading the YAML file and returning the namedtupleConfig. In the same file, let's implement the read_config function as follows:

def read_config():
current_dir = os.path.abspath(os.curdir)
file_path = os.path.join(current_dir, 'config.yaml')

try:
with open(file_path, mode='r', encoding='UTF-8') as file:
config = yaml.load(file)

config['base_url'] =
f'{config["api_url"]}/{config["api_version"]}'

auth_method = config['auth_method']
config['auth_method'] =
AuthMethod.__members__.get(auth_method)

return Config(**config)

except IOError as e:
print(""" Error: couldn''t file the configuration file
`config.yaml`
'on your current directory.

Default format is:',

client_id: 'your_client_id'
client_secret: 'you_client_secret'
access_token_url: 'https://accounts.spotify.com/api/token'
auth_url: 'http://accounts.spotify.com/authorize'
api_version: 'v1'
api_url: 'http//api.spotify.com'
auth_method: 'authentication method'

* auth_method can be CLIENT_CREDENTIALS or
AUTHORIZATION_CODE"""
)
raise

The read_config function starts off by using the os.path.abspath function to get the absolute path of the current directory, and assigns it to the current_dir variable. Then, we join the path stored on the current_dir variable with the name of the file, in this case, the YAML configuration file.

inside the try statement, we try to open the file as read-only and set the encoding to UTF-8. In the event this fails, it will print a help message to the user saying that it couldn't open the file and will show help describing how the YAML configuration file is structured.

If the configuration file can be read successfully, we call the load function in the yaml module to load and parse the file, and assign the results to the config variable. We also include an extra item in the config called base_url, which is just a helper value that contains the concatenated values of api_url and api_version.

The value of the base_url will look something like this: https://api.spotify.com/v1.

Lastly, we create an instance of Config. Note how we spread the values in the constructor; this is possible because the namedtupleConfig, has the same fields as the object returned by yaml.load(). This would be exactly  the same as doing this:

return Config(
client_id=config['client_id'],
client_secret=config['client_secret'],
access_token_url=config['access_token_url'],
auth_url=config['auth_url'],
api_version=config['api_version'],
api_url=config['api_url'],
base_url=config['base_url'],
auth_method=config['auth_method'])

The final touch here is to create a __init__.py file in the pytify/core directory and import the read_config function that we just created:

from .config import read_config