Airbnb Clone: building the console. Part. 1 (introduction)

Airbnb Clone: building the console. Part. 1 (introduction)

This is the first full web application project for every software engineering trainee at Alx Africa. It is a hands-on practice showing the implementation of all the fundamental concepts covered in the Alx high-level programming track.

This console is a command interpreter to manipulate data without a visual interface, like in a shell (perfect for development and debugging).

Disclaimer:

To my successors in Alx who are doing or are yet to do this project, the purpose of this article is to provide guidance and insight on how to approach this task. Please note that the solutions provided are not intended to be copied and pasted. You are tasked with coming up with solutions by yourself to meet the learning objectives. You are aware that plagiarism is strictly forbidden, and any form of cheating or academic dishonesty is unacceptable. Cheating in learning is simply self-deception, and that's downright dangerous, honestly!

Also, remember Kimba (Alx plagiarism checker) and save yourself from being Kimbared!

While I will do my best to guide you through the task, I will not provide a complete solution. Instead, I will be available to tutor and have a totally "free" learning session with anyone who wishes to learn. You can reach out to me via Twitter or LinkedIn.

By continuing to read this article, you agree to adhere to the above guidelines and understand that any violation will result in your removal from the program. So without further ado, let's roll up our sleeves as usual and dive in.

A brief overview of the console

The console is a command interpreter similar to a Python standard, interactive shell, or REPL. It allows us to run commands and interact with the web application through the command-line interface (terminal). Through this command interpreter, we aim to have a way to handle the objects in our project. This includes creating new objects, such as a new user or a new place, retrieving objects from a file or database, performing operations on objects such as counting and computing statistics, updating object attributes, and finally destroying objects when necessary. It's also very useful to track down bugs or logic errors because we can run and experiment with our web application on the console with different inputs.

In a nutshell, "the console" refers to a command-line interface where we can interact with the objects of our web application by entering commands and receiving output.

Now, let me be more concise about the objects of our web application that I'm referring to.

We are building a web application where people can book a temporary apartment. Everything in Python is an object, which implies that they are an instance of a certain class. So, every entity in our Airbnb web application, including a person or people who want to book an apartment using this web application we are building in Python will be represented as an instance of a certain class in the database. An instance (object) of the User class in our database represents an individual who has created an account on this web app and contains relevant information such as name, email, and password. then an instance (object) of the class State represents a state or province, while an instance of the class City represents a city within that state, there will also have an instance of the class Place that represents a temporary room or suite available for booking on the web app and contains relevant information such as its name, description, location, price, and availability dates.

I will use the User class and its instance to explain extensively now:

When a user wants to book an apartment, he or she will first create an account on our Airbnb web application, and an instance of the User class will be created and stored in the database, containing information such as the user's name, email, and password which are used to authenticate the user, retrieve their personal information, and process their bookings.

So now, to test the flow of this implementation, we are building a console that will allow us to interact with the User object from our end as developers through a command interpreter. The console will provide a command-line interface where we, the developers, can create new User objects by inputting the required attribute values (such as name, email, and password), view the output to confirm that the object was properly instantiated, and modify the attributes to test the update functionality.

This way, we can ensure that the flow and implementation of these objects are working correctly and according to our expectations. As a user on the web application tries to modify their details, such as their password, we can see if the updates are being properly reflected in the User object.

Later in the practical implementation, we will write these classes I mentioned together with their required class attributes.

I just hope this will give you a clear understanding of what our console entails.

Below is a list of a few concepts you must familiarize yourself with to be able to build or do this project; also, practice and get yourself acquainted with the listed modules.

  1. Cmd Module: It is designed or used as a base class for command interpreters like the console application we are building. What does this entail? By default, the Cmd module uses the readline library to provide functionality for handling user input from the console. A few of the features and functionalities provided by the readline library are listed below:

-> Interactive prompt handling: readline allows the shell to display a prompt to the user, indicating that the shell is ready to accept input. The user can then type a command or other input, which the shell will interpret and execute.

-> Command line editing: readline provides functionality for editing the current command line (i.e., the line of text currently being entered by the user), including features such as moving the cursor, deleting characters, and inserting text.

-> Command completion: readline can also provide tab-completion functionality, which suggests possible completions for the current command or argument based on previously entered commands or other contextual information.

For example, if we are writing a class where we will define the logic and implement the behaviour of our console (command interpreter), this will look like this:

import cmd

class MyConsole(cmd.Cmd):
    prompt = "(Airbnb) "

I just defined a class for the console, and I set the prompt to (Airbnb) which means that when I run this file on my terminal, I will get a command prompt (Airbnb) just like a Python interactive shell prompt >> ready to take in commands and give outputs. Also, notice the whitespace before the last double quote; at least there should be a space after the prompt to separate your command interpreter prompt from the commands you will input.

You can also see that from this definition, class Myconsole(cmd.Cmd):, our console inherits from a base class, cmd.Cmd. This means that we now have access to the wide range of features and capabilities in the cmd module that can be customized and extended as needed, and one of them is the use of the readline library by the cmd module to provide functionality for handling user input from the console. This now makes it possible for us to implement or replicate these functionalities by defining some methods (methods in Python are somewhat similar to functions, except they are associated with objects and classes) in/under this class to implement our own logic according to the project requirements.

  1. uuid module: Uuid stands for "Universally Unique Identifier." It's a 128-bit identifier that is unique across both space and time. What this entails is that, when we implement it, we will be able to generate hexadecimal digits (959902c6-6bc1-46c9-8b78-e93d6d67aa8c) that are unique across space and time, and the probability of two UUIDs being the same is so low as being impossible. These digits are the ID we will use to identify our objects, and at some point in the project we will have a database, so we need something to uniquely identify the data in each row in our database.

    It is as simple as assigning uuid4() to the ID of every instance of your class.

     from uuid import uuid4
    
     class BaseModle:
         def __init__(self):
             self.id = str(uuid4())
    
     mod_1 = BaseModle()
     print(mod_1.id)
    
     # outputs
     41b3deff-4cf7-4c76-bdd4-2a565e82fa84
    

    You can see that the output gave us some hexadecimal digits, and this will be unique among the IDs of any instance of that class. You might also notice that I converted the UUID to a string; this is strictly recommended to ensure that it can be easily stored and retrieved as needed.

  2. dateTime module: The datetime module supplies us with classes for manipulating dates and times. One of the classes is datetime (yes, the datetime module has a class called datetime), and a few of the methods in the datetime class we will be making use of in this project are now(), strftime() or strptime(). we can also use isoformat() to achieve the same result as strptime() and strftime().

    Before now, I talked about the reason or need why we are building a console, where I said that "we aim to have a way to handle the objects in our project; this includes creating new objects, such as a new user or a new place, retrieving objects from a file or database, performing operations on objects such as counting and computing statistics, updating object attributes, and finally destroying objects when necessary." So, this module provides us with a convenient way to work with date and time values and allows us to accurately track the creation and modification times of each instance or object of our class. It provides a timestamp for each time we create a new instance or modify its attributes. I will explain extensively when we start writing the base model for this console, the logic involved in implementing this, and how we dynamically assign this module to every instance of our class so that it gives us the current timestamp when an instance (object) is created or modified. But here is a little snippet of it:

    First, we use the now() method in the datetime class to retrieve the current local date and time.

     from datetime import datetime
    
     class BaseModel:
         def __init__(self):
         # retrieve current local date and time
             self.created_at = datetime.now()
    
         def __str__(self):
             return "{}".format(self.__dict__)
    
     mod_1 = BaseModel()
     print(mod_1)
     print(type(mod_1.created_at))
    
     #outputs 
     {'created_at': datetime.datetime(2023, 5, 2, 19, 55, 43, 924663)}
     <class 'datetime.datetime'>
    

I created a dynamic attribute created_at, and assigned it a value at runtime in the constructor __init__() using the datetime.now() method, which gets the current date and time at the moment of instantiation. This means that the value of created_at will be different for each instance of BaseModel because it will be set to the time at which the instance was created. The datetime values you see in the output are year, month, day, hour, minute, seconds, and microseconds.

Then i defined a __str__() method to best showcase a visually convincing string representation of the output in dictionary format so that you can see that the timestamp is a value of the attribute, created_at which denotes the date and time the instance was created. Another thing to note is that this timestamp is of the datetime object type <class 'datetime.datetime'>. But at some point in this project, for storage purposes, we may want to format this object to a string object in ISO format: %Y-%m-%dT%H:%M:%S.%f (e.g: 2017-06-14T22:31:03.285259) or any other valid format of your choice, but for the sake of this project, we will be using this ISO format that is of the string object type: %Y-%m-%dT%H:%M:%S.%f. And that brings us to the use of isoformat() or strftime() and strptime() methods.

class BaseModel:
    def __init__(self):
        self.created_at = datetime.now()
        # convert to string object in ISO format
        self.created_at = self.created_at.strftime("%Y-%m-%dT%H:%M:%S.%f")

    def __str__(self):
        return "{}".format(self.__dict__)

mod_1 = BaseModel()
print(mod_1)
print(type(mod_1.created_at))

# outputs
{'created_at': '2023-05-02T21:16:50.249203'}
<class 'str'>

You can see from the output that it is now a str type object <class 'str'> instead of datetime.datetime object type. We can also achieve the same result by using isoformat(). We simply call the isoformat() method on the instance of datetime.now()

class BaseModel:
    def __init__(self):
        self.created_at = datetime.now()
        # convert to string object in ISO format using isoformat() method
        self.created_at = self.created_at.isoformat()
  1. *args and **kwags: seeing these for the first time in Python without knowing what they are was a little daunting because I thought they were pointers. Lol! But fear not! They are our friends. These are special parameters in Python, they are arbitrary arguments used when we are unsure of the number of arguments we will pass into our function. The names "args" and "kwargs" don't matter; they can be replaced with any name or valid identifier (variable) of your choice. What matters is the use of an asterisk (*) and double asterisks (**) to denote these special parameters.

    We will not be using the (*args) parameter in this project, but for the sake of a complete concept explanation, let's see what *args are all about. *args is for multiple numbers of positional arguments that will be parsed in later, maybe during instantiation (creation of object) in classes, while **kwags is for multiple numbers of keyworded or key-value paired arguments that will be parsed later, just like *args maybe during instantiation (creating an object) in classes.

     class BaseModel:
         def __init__(self):
             self.created_at = datetime.now()
             # convert to string object in ISO format using isoformat() method
             self.created_at = self.created_at.isoformat()
    
         def __init__(self, *args, **kwargs):
          # loop and access all the values parsed into args 
              if args:
                  for values in args:
                      print(values)
       # loop and access all the key-value paired values parsed into kwargs
              if kwargs:
                  for key, value in kwargs.items():
                      setattr(self, key, value)
    
         def __str__(self):
             return "\n".join([f"{key}: {value}" for key, value in self.__dict__.items()])
    

    Let's parse in values to these special parameters

      # parsing in multiple number of arguments
     mod_1 = BaseModel("these", "are", "multiple", "number", "of", "args")
      # parsing in multiple number of key-value paired arguments
     mod_2 = BaseModel(name="Julien Kimba", age="41")
     print("\n=== outputs of Kwargs below ===")
     print(mod_2)
    

    From the above code snippet, you can see that using the special parameter *args made it possible for me to parse in as many arguments as I wanted to the instance of my class. Likewise, with the keyworded special parameter **kwargs, I was able to parse in key-value paired arguments during the instantiation of my object.

    The if args: and if kwargs: are used to check if the parameters are not empty, then it iterates and grabs all the values in them. setattr() is a method used for assigning key-value paired items to an object. Our object is represented by self, which is an instance of the class. Then I defined a string representation, __str__() that will output the values of kwargs in a dictionary format, with each key paired with its value. Below is the output.

     these
     are
     multiple
     number
     of
     args
    
     === outputs of Kwargs below ===
     name: Julien Kimba
     age: 41
    
  2. Python packages: first, let's talk about a module, because a Python package is just a directory with a collection of different modules. A module is simply a .py file (your regular Python file) containing your Python code which might be made up of certain defined functions for a specific task. If we have one or more of these files in a directory, the directory in question is now a package. but it becomes a package only by us adding an empty __init__.py file in that directory. This is just a way to organize related modules together.

    One significant aspect is how we import a module or function from a package, by using the dot notation. For example, if I have a module called mymodule inside a package called mypackage, I can import it like this: from mypackage import mymodule. Alternatively, I can use the import statement followed by the package and module names, separated by dots: import mypackage.mymodule. Remember that mypackagehere is the directory containing my modules. I can also import specific functions from a module using the from keyword. For example, to import a function called myfunc from mymodule, I can use: from mypackage.mymodule import myfunc.

  3. unit testing: unit testing is a valuable tool for any software development, although it's not without its challenges and can be time-consuming. but this ensures correctness because, by testing individual functions and methods, you can catch errors and bugs before they make it to a more delicate stage of your application development where things might become more complex to figure out.

    Conclusion

    I've ended up not building or practically implementing these concepts in building the console application we aimed at, but i'm glad that throughout this article, I have provided a broad overview of the concepts and principles involved in building the console application for our project (Airbnb clone).

    Don't worry, in the next episode, we will delve deeper, where I will explore some practical examples, and provide step-by-step guidance on how to implement the above concepts to get our console project done. So, stay tuned for the next episode!

    You can check out Part 2 Here.

    Thanks very much!

    It's evident you've dedicated your time and attention to reading this article, which means you find my words compelling enough, and I must say, "Thank you."

    I would greatly appreciate any feedback or contributions, as it helps me improve and put out more informative content. Feel free to reach out to me via email, LinkedIn, or Twitter. and I am open to constructive criticism and eager to learn from your insights.