From d976a1d761adba4d73b8c8e364e8fd3af6d65df6 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Tue, 11 Feb 2025 21:08:37 +0300 Subject: [PATCH 001/101] docs(CHANGELOG.md): Added change log file --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000000..8f70a7d7fa25 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ + +## [Unreleased] + + + +## v2.0.0 - 2025-02-11 + +[Unreleased]: https://github.com/mwanzaalbert/AirBnB_clone_v2/compare/v2.0.0...HEAD From eb1305e1b5bbad2ceae12d7b92fd9f3f1dbc1345 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Tue, 11 Feb 2025 21:16:22 +0300 Subject: [PATCH 002/101] docs(AUTHORS): Added 'Albert Mwanza to authors' --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 6fb53b5277cf..cd9d36789702 100644 --- a/AUTHORS +++ b/AUTHORS @@ -2,3 +2,4 @@ Ezra Nobrega Justin Majetich +Albert Mwanza \ No newline at end of file From 169356dbc9b8542c9b1591745f377f42afaaf176 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Tue, 11 Feb 2025 21:23:08 +0300 Subject: [PATCH 003/101] docs(CHANGELOG.md): Updated to show commit links --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f70a7d7fa25..fb3fb79d79db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ +# CHANGELOG + ## [Unreleased] +### Documentation +- **AUTHORS:** Added 'Albert Mwanza to authors' ([eb1305e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/eb1305e1b5bbad2ceae12d7b92fd9f3f1dbc1345)) +- **CHANGELOG.md:** Added change log file ([d976a1d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d976a1d761adba4d73b8c8e364e8fd3af6d65df6)) + ## v2.0.0 - 2025-02-11 From f2dd2a296a9efdba2e8759482e1ddc2fd1a9c9b1 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Tue, 11 Feb 2025 21:24:09 +0300 Subject: [PATCH 004/101] docs(CHANGELOG.md): Updated to show commit links --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb3fb79d79db..bb1c9634fbd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Documentation - **AUTHORS:** Added 'Albert Mwanza to authors' ([eb1305e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/eb1305e1b5bbad2ceae12d7b92fd9f3f1dbc1345)) +- **CHANGELOG.md:** Updated to show commit links ([169356d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/169356dbc9b8542c9b1591745f377f42afaaf176)) - **CHANGELOG.md:** Added change log file ([d976a1d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d976a1d761adba4d73b8c8e364e8fd3af6d65df6)) From 0775e3f2899da6b49fbed57904ad7162e84a8c53 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 06:41:12 +0300 Subject: [PATCH 005/101] chore(tests/__init__): Added executable rights --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py old mode 100644 new mode 100755 From 8f2a484529f40376cd4aaa6349ecd02156348ac0 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 06:53:40 +0300 Subject: [PATCH 006/101] docs(CHANGELOG.md): Updated commit links --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb1c9634fbd2..bcc76141c383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Documentation - **AUTHORS:** Added 'Albert Mwanza to authors' ([eb1305e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/eb1305e1b5bbad2ceae12d7b92fd9f3f1dbc1345)) +- **CHANGELOG.md:** Updated to show commit links ([f2dd2a2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f2dd2a296a9efdba2e8759482e1ddc2fd1a9c9b1)) - **CHANGELOG.md:** Updated to show commit links ([169356d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/169356dbc9b8542c9b1591745f377f42afaaf176)) - **CHANGELOG.md:** Added change log file ([d976a1d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d976a1d761adba4d73b8c8e364e8fd3af6d65df6)) From ad891f6bae73541cf1153c37646596dd0bcb6f94 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 06:59:33 +0300 Subject: [PATCH 007/101] refactor(console.py): Refactored exit() with return True in do_quit method --- console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console.py b/console.py index 13a8af68e930..daf6b5d65278 100755 --- a/console.py +++ b/console.py @@ -94,7 +94,7 @@ def postcmd(self, stop, line): def do_quit(self, command): """ Method to exit the HBNB console""" - exit() + return True def help_quit(self): """ Prints the help documentation for quit """ From d534c1e7a511d17424e8fcbdbe4c5ea7985d30a6 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 07:01:47 +0300 Subject: [PATCH 008/101] refactor(console.py): Refactored exit() and print() with return True in do_EOF method --- console.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/console.py b/console.py index daf6b5d65278..40b377640213 100755 --- a/console.py +++ b/console.py @@ -102,8 +102,7 @@ def help_quit(self): def do_EOF(self, arg): """ Handles EOF to exit program """ - print() - exit() + return True def help_EOF(self): """ Prints the help documentation for EOF """ From 8fcd9e75692ff88124b7e5fa148e98db35337d0f Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 07:05:16 +0300 Subject: [PATCH 009/101] refactor(console.py): Refactored storage import from models --- console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console.py b/console.py index 40b377640213..5b4c119101b5 100755 --- a/console.py +++ b/console.py @@ -3,7 +3,7 @@ import cmd import sys from models.base_model import BaseModel -from models.__init__ import storage +from models import storage from models.user import User from models.place import Place from models.state import State From f34c4e0f3c21d8c0d5996b87cae25969fe1323ea Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:05:13 +0300 Subject: [PATCH 010/101] chore(setup_mysql_dev.sql): Added MySQL setup file for development --- setup_mysql_dev.sql | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 setup_mysql_dev.sql diff --git a/setup_mysql_dev.sql b/setup_mysql_dev.sql new file mode 100644 index 000000000000..aa64d15c9944 --- /dev/null +++ b/setup_mysql_dev.sql @@ -0,0 +1,7 @@ +-- Prepares the server for the project +-- Creates a database hbnb_dev_db and a new user hbnb_dev +CREATE DATABASE IF NOT EXISTS hbnb_dev_db; +CREATE USER IF NOT EXISTS 'hbnb_dev'@'localhost' IDENTIFIED BY 'hbnb_dev_pwd'; +GRANT ALL PRIVILEGES ON hbnb_dev_db.* TO 'hbnb_dev'@'localhost'; +GRANT SELECT ON performance_schema.* TO 'hbnb_dev'@'localhost'; +FLUSH PRIVILEGES; From 59eeb6eed7268f39d8ed6d3f66d8fcaab73e3dbd Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:06:14 +0300 Subject: [PATCH 011/101] chore(setup_mysql_test.sql): Added MySQL setup file for testing --- setup_mysql_test.sql | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 setup_mysql_test.sql diff --git a/setup_mysql_test.sql b/setup_mysql_test.sql new file mode 100644 index 000000000000..3e35533d508d --- /dev/null +++ b/setup_mysql_test.sql @@ -0,0 +1,8 @@ +-- Prepares the server for the project +-- Creates a database hbnb_test_db and a new user hbnb_test +CREATE DATABASE IF NOT EXISTS hbnb_test_db; +CREATE USER IF NOT EXISTS 'hbnb_test'@'localhost' IDENTIFIED BY 'hbnb_test_pwd'; +GRANT USAGE ON *.* TO 'hbnb_test'@'localhost'; +GRANT ALL PRIVILEGES ON hbnb_test_db.* TO 'hbnb_test'@'localhost'; +GRANT SELECT ON performance_schema.* TO 'hbnb_test'@'localhost'; +FLUSH PRIVILEGES; From a295eb852a2484a9cee6ea6cc7a438da7d7f08b0 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:16:13 +0300 Subject: [PATCH 012/101] feat(models/engine/file_storage.py): Added a new public instance method to delete objects from storage --- models/engine/file_storage.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py index 6f5d7f8d4680..e2c186942676 100644 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -48,3 +48,11 @@ def reload(self): self.all()[key] = classes[val['__class__']](**val) except FileNotFoundError: pass + + def delete(self, obj=None): + """Delete object from storage.""" + if obj is not None: + key = f"{obj.__class__.__name__}.{obj.id}" + if key in self.__objects: + del self.__objects[key] + self.save() From c773e45d2e211e11afabf3ba17e27732211df90f Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:22:43 +0300 Subject: [PATCH 013/101] feat(models/engine/file_storage.py): Updated all method to allow filtering based on class --- models/engine/file_storage.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py index e2c186942676..acfd7e50fad8 100644 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 """This module defines a class to manage file storage for hbnb clone""" import json +import importlib class FileStorage: @@ -8,9 +9,36 @@ class FileStorage: __file_path = 'file.json' __objects = {} - def all(self): + def all(self, cls=None): """Returns a dictionary of models currently in storage""" - return FileStorage.__objects + classes = {'BaseModel', + 'User', 'Place', + 'State', 'City', 'Amenity', + 'Review' + } + + if cls is not None: + if isinstance(cls, str): + if cls not in classes: + return {} + try: + # Dynamically import the class + module_path = f"models.{cls.lower()}" # Convert class name to module name + module = importlib.import_module(module_path) + cls = getattr(module, cls) # Get the class from the module + + except (ImportError, AttributeError): + print("** class doesn't exist **") + + + # Filter objects based on the class + return { + key: value + for key, value in self.__objects.items() + if isinstance(value, cls) + } + + return self.__objects def new(self, obj): """Adds new object to storage dictionary""" From 6578f5a62b851b777b9a473772903461bb399a50 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:27:01 +0300 Subject: [PATCH 014/101] refactor(models/engine/file_storage.py): Refactored save method to use f-strings for key and self to update FileStorage__objects --- models/engine/file_storage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py index acfd7e50fad8..67a3f5408f50 100644 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -42,7 +42,8 @@ def all(self, cls=None): def new(self, obj): """Adds new object to storage dictionary""" - self.all().update({obj.to_dict()['__class__'] + '.' + obj.id: obj}) + key = f"{obj.__class__.__name__}.{obj.id}" + self.__objects[key] = obj def save(self): """Saves storage dictionary to file""" From 9303c938763d67828d1766b4e9044c084f2c4e38 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:30:06 +0300 Subject: [PATCH 015/101] refactor(models/engine/file_storage.py): Refactored save method to use dictionary comprehension --- models/engine/file_storage.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py index 67a3f5408f50..4deacacedb4d 100644 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -47,12 +47,13 @@ def new(self, obj): def save(self): """Saves storage dictionary to file""" - with open(FileStorage.__file_path, 'w') as f: - temp = {} - temp.update(FileStorage.__objects) - for key, val in temp.items(): - temp[key] = val.to_dict() - json.dump(temp, f) + with open(self.__file_path, 'w', encoding='utf-8') as outfile: + temp = { + key: value.to_dict() + for key, value in self.__objects.items() + } + + json.dump(temp, outfile) def reload(self): """Loads storage dictionary from file""" From 8d33ae48948ed6055f5200f7e9eb5fe1ff22da6f Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:30:06 +0300 Subject: [PATCH 016/101] refactor(models/engine/file_storage.py): Refactored save method to use dictionary comprehension --- models/engine/file_storage.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py index 67a3f5408f50..4deacacedb4d 100644 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -47,12 +47,13 @@ def new(self, obj): def save(self): """Saves storage dictionary to file""" - with open(FileStorage.__file_path, 'w') as f: - temp = {} - temp.update(FileStorage.__objects) - for key, val in temp.items(): - temp[key] = val.to_dict() - json.dump(temp, f) + with open(self.__file_path, 'w', encoding='utf-8') as outfile: + temp = { + key: value.to_dict() + for key, value in self.__objects.items() + } + + json.dump(temp, outfile) def reload(self): """Loads storage dictionary from file""" From 62bc9ce2b5123066a4c311b7abc5eb7c00811d64 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:43:13 +0300 Subject: [PATCH 017/101] refactor(models/engine/file_storage.py): Refactored reload method to use self to access FileStorage__file_path and to load data to FileStorage__objects --- models/engine/file_storage.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py index 4deacacedb4d..fa70a47c49c7 100644 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -65,17 +65,19 @@ def reload(self): from models.amenity import Amenity from models.review import Review - classes = { - 'BaseModel': BaseModel, 'User': User, 'Place': Place, + classes = {'BaseModel': BaseModel, + 'User': User, 'Place': Place, 'State': State, 'City': City, 'Amenity': Amenity, 'Review': Review } try: - temp = {} - with open(FileStorage.__file_path, 'r') as f: - temp = json.load(f) - for key, val in temp.items(): - self.all()[key] = classes[val['__class__']](**val) + with open(self.__file_path, 'r', encoding='utf-8') as infile: + file_data = json.load(infile) + + for key, value in file_data.items(): + cls_name = value['__class__'] + self.__objects[key] = classes[cls_name](**value) + except FileNotFoundError: pass From fcedbb650d4eca90d2952dabdd50f77812893c9e Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:48:17 +0300 Subject: [PATCH 018/101] style(models/engine/file_storage.py): Pycodestyle Formatting --- models/engine/file_storage.py | 51 ++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py index fa70a47c49c7..3cde9320f9c2 100644 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -1,62 +1,63 @@ #!/usr/bin/python3 -"""This module defines a class to manage file storage for hbnb clone""" +"""This module defines a class to manage file storage for hbnb clone.""" import json import importlib class FileStorage: - """This class manages storage of hbnb models in JSON format""" + """This class manages storage of hbnb models in JSON format.""" + __file_path = 'file.json' __objects = {} def all(self, cls=None): - """Returns a dictionary of models currently in storage""" + """Return a dictionary of models currently in storage.""" classes = {'BaseModel', - 'User', 'Place', - 'State', 'City', 'Amenity', - 'Review' - } - + 'User', 'Place', + 'State', 'City', 'Amenity', + 'Review' + } + if cls is not None: if isinstance(cls, str): if cls not in classes: return {} try: - # Dynamically import the class - module_path = f"models.{cls.lower()}" # Convert class name to module name + # Dynamically import the class and + # Convert class name to module name + module_path = f"models.{cls.lower()}" module = importlib.import_module(module_path) cls = getattr(module, cls) # Get the class from the module - + except (ImportError, AttributeError): print("** class doesn't exist **") - - + # Filter objects based on the class return { key: value for key, value in self.__objects.items() if isinstance(value, cls) - } - + } + return self.__objects def new(self, obj): - """Adds new object to storage dictionary""" + """Add new object to storage dictionary.""" key = f"{obj.__class__.__name__}.{obj.id}" self.__objects[key] = obj def save(self): - """Saves storage dictionary to file""" + """Save storage dictionary to file.""" with open(self.__file_path, 'w', encoding='utf-8') as outfile: temp = { key: value.to_dict() for key, value in self.__objects.items() - } - + } + json.dump(temp, outfile) def reload(self): - """Loads storage dictionary from file""" + """Load storage dictionary from file.""" from models.base_model import BaseModel from models.user import User from models.place import Place @@ -66,14 +67,14 @@ def reload(self): from models.review import Review classes = {'BaseModel': BaseModel, - 'User': User, 'Place': Place, - 'State': State, 'City': City, 'Amenity': Amenity, - 'Review': Review - } + 'User': User, 'Place': Place, + 'State': State, 'City': City, 'Amenity': Amenity, + 'Review': Review + } try: with open(self.__file_path, 'r', encoding='utf-8') as infile: file_data = json.load(infile) - + for key, value in file_data.items(): cls_name = value['__class__'] self.__objects[key] = classes[cls_name](**value) From a313ed641efa0f18402ecc6c831978ba7df1d4be Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:50:17 +0300 Subject: [PATCH 019/101] chore(models/engine/file_storage.py): Added executable rights --- models/engine/file_storage.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 models/engine/file_storage.py diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py old mode 100644 new mode 100755 From c1467b9d0aec857238d37ccbba2a699e77729028 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:51:17 +0300 Subject: [PATCH 020/101] chore(models/engine/__init__.py): Added executable rights --- models/engine/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 models/engine/__init__.py diff --git a/models/engine/__init__.py b/models/engine/__init__.py old mode 100644 new mode 100755 From b76715720d13b1949e35de2edb8bfaef2743e959 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 08:55:51 +0300 Subject: [PATCH 021/101] chore(models/__init_.py): Added encoding declaration --- models/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/__init__.py b/models/__init__.py index d3765c2bc603..a3ea9a0937fa 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,7 +1,8 @@ #!/usr/bin/python3 +# -*- coding: utf-8 -*- """This module instantiates an object of class FileStorage""" from models.engine.file_storage import FileStorage storage = FileStorage() -storage.reload() +storage.reload() \ No newline at end of file From 024416205d33711e30ace48b9f859c66ac29c991 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:01:12 +0300 Subject: [PATCH 022/101] feat(models/__init__.py): Updated to instantiate storage based on the environment variable HBNB_TYPE_STORAGE --- models/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/models/__init__.py b/models/__init__.py index a3ea9a0937fa..102748b5b104 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,8 +1,16 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- -"""This module instantiates an object of class FileStorage""" -from models.engine.file_storage import FileStorage +"""This module instantiates an object of class FileStorage or DBStorage +based on the value of the evironment variable "HBNB_TYPE_STORAGE." +""" +import os +if os.getenv("HBNB_TYPE_STORAGE") == "db": + from models.engine.db_storage import DBStorage + storage = DBStorage() -storage = FileStorage() -storage.reload() \ No newline at end of file +else: + from models.engine.file_storage import FileStorage + storage = FileStorage() + +storage.reload() From 38af25eae816e460a712f2d558bbeb66ca8306f8 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:02:22 +0300 Subject: [PATCH 023/101] chore(models/__init__.py): Added executable rights --- models/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 models/__init__.py diff --git a/models/__init__.py b/models/__init__.py old mode 100644 new mode 100755 From 37d9454b4bf03ec3917649dbdced3c9fad4c64ec Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:04:13 +0300 Subject: [PATCH 024/101] chore(models/engine/file_storage.py): Added encoding declaration --- models/engine/file_storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/models/engine/file_storage.py b/models/engine/file_storage.py index 3cde9320f9c2..7eb2f0397689 100755 --- a/models/engine/file_storage.py +++ b/models/engine/file_storage.py @@ -1,4 +1,5 @@ #!/usr/bin/python3 +# -*- coding: utf-8 -*- """This module defines a class to manage file storage for hbnb clone.""" import json import importlib From 50988b8fd61e56f62a28478db7072225a2fa9c66 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:26:06 +0300 Subject: [PATCH 025/101] feat(models/engine/db_storage.py): Added DBStorage module for managing database storage in MySQL --- models/engine/db_storage.py | 170 ++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100755 models/engine/db_storage.py diff --git a/models/engine/db_storage.py b/models/engine/db_storage.py new file mode 100755 index 000000000000..a2d749c566a0 --- /dev/null +++ b/models/engine/db_storage.py @@ -0,0 +1,170 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +DBStorage module for managing database storage in MySQL. + +This module defines the `DBStorage` class, which provides methods +to interact with a MySQL database using SQLAlchemy. +""" +import os +import importlib +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.engine import URL +from sqlalchemy.exc import OperationalError +from models.base_model import Base +from models.amenity import Amenity +from models.city import City +from models.place import Place, place_amenity +from models.review import Review +from models.state import State +from models.user import User + +__author__ = "Albert Mwanza" +__license__ = "MIT" +__date__ = "2025-02-12" +__version__ = "2.1" + + +class DBStorage: + """ + This class manages storage of hbnb models in a MySQL database. + + Attributes_: + __engine (sqlalchemy.engine.Engine): The database engine. + __session (sqlalchemy.orm.scoped_session): The database session. + """ + + DB_CLASSES = [User, State, City, Amenity, Place, Review] + + __engine: None + __session: None + + __objects = {} + + def __init__(self): + """ + Initialize the DBStorage instance and set up the database connection. + + The connection URL is constructed using environment variables: + - HBNB_MYSQL_USER: MySQL username + - HBNB_MYSQL_PWD: MySQL password + - HBNB_MYSQL_HOST: MySQL host + - HBNB_MYSQL_DB: MySQL database name + - HBNB_ENV: Application environment (e.g., 'test') + + If `HBNB_ENV` is set to 'test', all tables are dropped. + """ + # Construct the database connection URL + url_object = URL.create( + "mysql+mysqldb", # dialect+driver + username=os.getenv("HBNB_MYSQL_USER"), + password=os.getenv("HBNB_MYSQL_PWD"), + host=os.getenv("HBNB_MYSQL_HOST"), + database=os.getenv("HBNB_MYSQL_DB"),) + + # Create the database engine + self.__engine = create_engine(url_object, pool_pre_ping=True) + + # Drop all tables if in test environment + if os.getenv("HBNB_ENV") == "test": + try: + # Bind the engine to the Base's metadata + Base.metadata.bind = self.__engine + + # Drop all tables defined in the Base + Base.metadata.drop_all(self.__engine) + + except OperationalError as e: + message = getattr(e, '_message') + print(message().partition(" ") + [-1].strip('()').split(',')[-1]) + + def all(self, cls=None): + """Return a dictionary of models currently in storage.""" + if cls is not None: + classes = ('BaseModel', + 'User', + 'Place', + 'State', + 'City', + 'Amenity', + 'Review') + + if isinstance(cls, str): + if cls not in classes: + return {} + + try: + # Dynamically import the class + # Convert class name to module name + module_path = f"models.{cls.lower()}" + module = importlib.import_module(module_path) + # Get the class from the module + cls = getattr(module, cls) + + except (ImportError, AttributeError): + print("** class doesn't exist **") + + if cls in self.DB_CLASSES: + for obj in self.__session.query(cls).all(): + key = f"{type(obj).__name__}.{obj.id}" + self.__objects[key] = obj + else: + return {} + else: + for model in self.DB_CLASSES: + for obj in self.__session.query(model).all(): + key = f"{type(obj).__name__}.{obj.id}" + self.__objects[key] = obj + + return self.__objects + + def new(self, obj): + """Add new object to storage dictionary.""" + self.__session.add(obj) + + def save(self): + """Save storage dictionary to file.""" + self.__session.commit() + + def reload(self): + """Load storage dictionary from file.""" + try: + # Bind the engine to the Base's metadata + Base.metadata.bind = self.__engine + + Base.metadata.create_all(self.__engine) + + session_factory = sessionmaker(bind=self.__engine, + expire_on_commit=False) + + Session = scoped_session(session_factory) + + except OperationalError as e: + message = getattr(e, '_message') + print(message().partition(" ") + [-1].strip('()').split(',')[-1]) + else: + self.__session = Session() + + def delete(self, obj=None): + """Delete object from the storage.""" + if obj is not None: + self.__session.delete(obj) + + try: + del self.__objects[type(obj).__name__ + '.' + obj.id] + + except KeyError as e: + pass + + self.save() + + def close(self): + """Close the working SQLAlchemy session.""" + self.__session.close() + + def rollback(self): + """Roll back the session to clear the invalid state.""" + self.__session.rollback() From a2cbcac86ad913badb8a23cb7b073dbfcf93f10c Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:32:36 +0300 Subject: [PATCH 026/101] feat(models/basem_model.py): Added class attributes for SQLAlchemy table mapping --- models/base_model.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) mode change 100644 => 100755 models/base_model.py diff --git a/models/base_model.py b/models/base_model.py old mode 100644 new mode 100755 index 4856e9de421f..b19b3c7bb101 --- a/models/base_model.py +++ b/models/base_model.py @@ -1,11 +1,26 @@ #!/usr/bin/python3 +# -*- coding: utf-8 -*- """This module defines a base class for all models in our hbnb clone""" +import os import uuid from datetime import datetime +from sqlalchemy import Column, String, DateTime +from sqlalchemy.ext.declarative import declarative_base + + +Base = declarative_base() class BaseModel: """A base class for all hbnb models""" + + __abstract__ = True + + # Mark this class as an abstract to prevent its table creation + id = Column(String(60), nullable=False, primary_key=True, unique=True) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + updated_at = Column(DateTime, nullable=False, default=datetime.utcnow) + def __init__(self, *args, **kwargs): """Instatntiates a new model""" if not kwargs: From 102ac81446aea075cbb4d3b03624709148da316c Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:32:36 +0300 Subject: [PATCH 027/101] feat(models/base_model.py): Added class attributes for SQLAlchemy table mapping chore(models/base_model.py): Added executable rights --- models/base_model.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) mode change 100644 => 100755 models/base_model.py diff --git a/models/base_model.py b/models/base_model.py old mode 100644 new mode 100755 index 4856e9de421f..b19b3c7bb101 --- a/models/base_model.py +++ b/models/base_model.py @@ -1,11 +1,26 @@ #!/usr/bin/python3 +# -*- coding: utf-8 -*- """This module defines a base class for all models in our hbnb clone""" +import os import uuid from datetime import datetime +from sqlalchemy import Column, String, DateTime +from sqlalchemy.ext.declarative import declarative_base + + +Base = declarative_base() class BaseModel: """A base class for all hbnb models""" + + __abstract__ = True + + # Mark this class as an abstract to prevent its table creation + id = Column(String(60), nullable=False, primary_key=True, unique=True) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + updated_at = Column(DateTime, nullable=False, default=datetime.utcnow) + def __init__(self, *args, **kwargs): """Instatntiates a new model""" if not kwargs: From d78801164042c4308baa832ec00bbb8bc922b554 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:46:22 +0300 Subject: [PATCH 028/101] refactor(models/base_model.py): Refactored __init__ to iterate over kwargs and update attributes using setattr function. Removed storage import from __init__ and calling of the storage.new method --- models/base_model.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/models/base_model.py b/models/base_model.py index b19b3c7bb101..c46a84992bff 100755 --- a/models/base_model.py +++ b/models/base_model.py @@ -22,21 +22,22 @@ class BaseModel: updated_at = Column(DateTime, nullable=False, default=datetime.utcnow) def __init__(self, *args, **kwargs): - """Instatntiates a new model""" - if not kwargs: - from models import storage - self.id = str(uuid.uuid4()) - self.created_at = datetime.now() - self.updated_at = datetime.now() - storage.new(self) - else: - kwargs['updated_at'] = datetime.strptime(kwargs['updated_at'], - '%Y-%m-%dT%H:%M:%S.%f') - kwargs['created_at'] = datetime.strptime(kwargs['created_at'], - '%Y-%m-%dT%H:%M:%S.%f') - del kwargs['__class__'] - self.__dict__.update(kwargs) - + """Instantiates a new model""" + self.id = str(uuid.uuid4()) + self.created_at = datetime.now() + self.updated_at = datetime.now() + + + if kwargs: + for key, value in kwargs.items(): + if key != "__class__": + if key in ("created_at", "updated_at"): + setattr(self, key, + datetime.strptime(kwargs[f'{key}'], + '%Y-%m-%dT%H:%M:%S.%f')) + else: + setattr(self, key, value) + def __str__(self): """Returns a string representation of the instance""" cls = (str(type(self)).split('.')[-1]).split('\'')[0] From a38b99eb136a0e19b75a9042dd08dfb9cffee101 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:50:26 +0300 Subject: [PATCH 029/101] refactor(models/base_model.py): Refactored __str__ to use f-strings for the return string and to removed the _sa_instance_state attribute from the string representation --- models/base_model.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/models/base_model.py b/models/base_model.py index c46a84992bff..2e52221f40a4 100755 --- a/models/base_model.py +++ b/models/base_model.py @@ -40,8 +40,15 @@ def __init__(self, *args, **kwargs): def __str__(self): """Returns a string representation of the instance""" - cls = (str(type(self)).split('.')[-1]).split('\'')[0] - return '[{}] ({}) {}'.format(cls, self.id, self.__dict__) + __dict__copy = self.__dict__.copy() + + try: + __dict__copy.pop('_sa_instance_state') + + except KeyError: + pass + + return f"[{type(self).__name__}] ({self.id}) {__dict__copy}" def save(self): """Updates updated_at with current time when instance is changed""" From 71c19ffb08968bf732fb69b5df4fad925a292b89 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:53:17 +0300 Subject: [PATCH 030/101] refactor(models/base_model.py): Refactored save method to call storage.new method --- models/base_model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/base_model.py b/models/base_model.py index 2e52221f40a4..8e918f4fa1e2 100755 --- a/models/base_model.py +++ b/models/base_model.py @@ -53,7 +53,9 @@ def __str__(self): def save(self): """Updates updated_at with current time when instance is changed""" from models import storage + self.updated_at = datetime.now() + storage.new(self) storage.save() def to_dict(self): From ce431f5764a302ef897b638dd5c8e6bd1016f17c Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:56:23 +0300 Subject: [PATCH 031/101] refactor(models/base_model.py): Refactored to_dict method to iterate over __dict__ attribute and ignore the _sa_instance_state attribute --- models/base_model.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/models/base_model.py b/models/base_model.py index 8e918f4fa1e2..fa5171f22500 100755 --- a/models/base_model.py +++ b/models/base_model.py @@ -60,10 +60,13 @@ def save(self): def to_dict(self): """Convert instance into dict format""" - dictionary = {} - dictionary.update(self.__dict__) - dictionary.update({'__class__': - (str(type(self)).split('.')[-1]).split('\'')[0]}) - dictionary['created_at'] = self.created_at.isoformat() - dictionary['updated_at'] = self.updated_at.isoformat() - return dictionary + return_dict = {"__class__": type(self).__name__} + + for key, value in self.__dict__.copy().items(): + if key != '_sa_instance_state': # updates + if key in ("created_at", "updated_at"): + return_dict[key] = value.isoformat() + else: + return_dict[key] = value + + return return_dict From 984e47315aa3f5aae562a95de6f3869763c098b4 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 09:58:38 +0300 Subject: [PATCH 032/101] feat(models/base_model.py): Added delete method to call storage.delete method to delete object from storage --- models/base_model.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/models/base_model.py b/models/base_model.py index fa5171f22500..b54fffc132fc 100755 --- a/models/base_model.py +++ b/models/base_model.py @@ -70,3 +70,9 @@ def to_dict(self): return_dict[key] = value return return_dict + + def delete(self): + """delete the current instance from the storage""" + from models import storage + + storage.delete(self) # updates \ No newline at end of file From 96d660f5e222d36043aaf01be8779f13dbc17aac Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 10:04:26 +0300 Subject: [PATCH 033/101] style(models/base_model.py): pycodestyle formatting --- models/base_model.py | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/models/base_model.py b/models/base_model.py index b54fffc132fc..d7489b72b61a 100755 --- a/models/base_model.py +++ b/models/base_model.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- -"""This module defines a base class for all models in our hbnb clone""" -import os +"""This module defines common attributes and methods for all hbnb models.""" import uuid from datetime import datetime from sqlalchemy import Column, String, DateTime @@ -12,9 +11,20 @@ class BaseModel: - """A base class for all hbnb models""" + """ + A base class for all hbnb models. - __abstract__ = True + This class defines common attributes and methods for all models + that inherit from it. It is marked as an abstract class to prevent + direct table creation. + + Attributes_: + id (Column): A unique identifier for each instance. + created_at (Column): Timestamp when the instance was created. + updated_at (Column): Timestamp when the instance was last updated. + """ + + __abstract__ = True # Prevents table creation for BaseModel # Mark this class as an abstract to prevent its table creation id = Column(String(60), nullable=False, primary_key=True, unique=True) @@ -22,12 +32,11 @@ class BaseModel: updated_at = Column(DateTime, nullable=False, default=datetime.utcnow) def __init__(self, *args, **kwargs): - """Instantiates a new model""" + """Instantiate a new model.""" self.id = str(uuid.uuid4()) self.created_at = datetime.now() self.updated_at = datetime.now() - - + if kwargs: for key, value in kwargs.items(): if key != "__class__": @@ -37,21 +46,21 @@ def __init__(self, *args, **kwargs): '%Y-%m-%dT%H:%M:%S.%f')) else: setattr(self, key, value) - + def __str__(self): - """Returns a string representation of the instance""" + """Return a string representation of the instance.""" __dict__copy = self.__dict__.copy() - + try: __dict__copy.pop('_sa_instance_state') - + except KeyError: pass - + return f"[{type(self).__name__}] ({self.id}) {__dict__copy}" def save(self): - """Updates updated_at with current time when instance is changed""" + """Update updated_at with current time when instance is changed.""" from models import storage self.updated_at = datetime.now() @@ -59,11 +68,11 @@ def save(self): storage.save() def to_dict(self): - """Convert instance into dict format""" + """Convert instance into dict format.""" return_dict = {"__class__": type(self).__name__} for key, value in self.__dict__.copy().items(): - if key != '_sa_instance_state': # updates + if key != '_sa_instance_state': # updates if key in ("created_at", "updated_at"): return_dict[key] = value.isoformat() else: @@ -72,7 +81,7 @@ def to_dict(self): return return_dict def delete(self): - """delete the current instance from the storage""" + """Delete instance from the storage.""" from models import storage - storage.delete(self) # updates \ No newline at end of file + storage.delete(self) # updates From 52863e9e8176df7496cb0881edc23ab0d74c1153 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 10:21:57 +0300 Subject: [PATCH 034/101] Regenerate CHANGELOG.md --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcc76141c383..e86c552656b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,21 @@ ## [Unreleased] +### Chores +- **setup_mysql_dev.sql:** Added MySQL setup file for development +- **setup_mysql_test.sql:** Added MySQL setup file for testing + + + +## [v2.1.0] - 2025-02-12 +### Code Refactoring +- **console.py:** Refactored storage import from models ([8fcd9e7](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8fcd9e75692ff88124b7e5fa148e98db35337d0f)) +- **console.py:** Refactored exit() and print() with return True in do_EOF method ([d534c1e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d534c1e7a511d17424e8fcbdbe4c5ea7985d30a6)) +- **console.py:** Refactored exit() with return True in do_quit method ([ad891f6](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ad891f6bae73541cf1153c37646596dd0bcb6f94)) + ### Documentation - **AUTHORS:** Added 'Albert Mwanza to authors' ([eb1305e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/eb1305e1b5bbad2ceae12d7b92fd9f3f1dbc1345)) +- **CHANGELOG.md:** Updated commit links ([8f2a484](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8f2a484529f40376cd4aaa6349ecd02156348ac0)) - **CHANGELOG.md:** Updated to show commit links ([f2dd2a2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f2dd2a296a9efdba2e8759482e1ddc2fd1a9c9b1)) - **CHANGELOG.md:** Updated to show commit links ([169356d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/169356dbc9b8542c9b1591745f377f42afaaf176)) - **CHANGELOG.md:** Added change log file ([d976a1d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d976a1d761adba4d73b8c8e364e8fd3af6d65df6)) @@ -13,4 +26,5 @@ ## v2.0.0 - 2025-02-11 -[Unreleased]: https://github.com/mwanzaalbert/AirBnB_clone_v2/compare/v2.0.0...HEAD +[Unreleased]: https://github.com/mwanzaalbert/AirBnB_clone_v2/compare/v2.1.0...HEAD +[v2.1.0]: https://github.com/mwanzaalbert/AirBnB_clone_v2/compare/v2.0.0...v2.1.0 From 764f1696da024773b87ef5f6dd61d39d82e37504 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 10:38:06 +0300 Subject: [PATCH 035/101] feat(models/amenity.py): Updated class Amenity to Inherit from BaseModel and Base. Added class attributes for SQLAlchemy table mapping and Many to Many relationship mapping with Places --- models/amenity.py | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/models/amenity.py b/models/amenity.py index a181095e4170..a881057b087b 100644 --- a/models/amenity.py +++ b/models/amenity.py @@ -1,7 +1,39 @@ #!/usr/bin/python3 -""" State Module for HBNB project """ -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +Amenity module for the hbnb project. +Defines the `Amenity` class, which represents amenities available for places. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy.orm import relationship +from sqlalchemy import Column, String -class Amenity(BaseModel): - name = "" + +class Amenity(BaseModel, Base): + """ + Represents an amenity in the hbnb project. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table. + name (Column or str): The name of the amenity. + place_amenities (relationship): A many-to-many relationship with + `Place` (only for DB storage). + """ + + __tablename__ = "amenities" + + name = Column(String(128), nullable=False) + + if os.getenv("HBNB_TYPE_STORAGE") == 'db': + place_amenities = relationship('Place', + secondary='place_amenity', + overlaps="place_amenities") + else: + name: str = "" From 2d497fb0d83a773c81a1266c4e4f9c4c460d1fe3 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 10:39:14 +0300 Subject: [PATCH 036/101] chore(models/amenity.py): Added executable rights --- models/amenity.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 models/amenity.py diff --git a/models/amenity.py b/models/amenity.py old mode 100644 new mode 100755 From a8c7707715761e1e3b80ee070402311ccc36d82d Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 10:58:01 +0300 Subject: [PATCH 037/101] feat(models/city.py): Added class attributes for SQLAlchemy table mapping and relationship mapping --- models/city.py | 48 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/models/city.py b/models/city.py index b9b4fe221502..3bffb54c215a 100644 --- a/models/city.py +++ b/models/city.py @@ -1,9 +1,45 @@ #!/usr/bin/python3 -""" City Module for HBNB project """ -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +City module for the HBNB project. +Defines the `City` class, which represents a city associated with a state. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy.orm import relationship +from sqlalchemy import Column, String, ForeignKey -class City(BaseModel): - """ The city class, contains state ID and name """ - state_id = "" - name = "" + +class City(BaseModel, Base): + """ + Represents a city in the HBNB project. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for cities. + name (Column or str): The name of the city (string, required). + state_id (Column or str): The ID of the associated state (foreign key, + required). + places (relationship): A one-to-many relationship with `Place` + (only for DB storage). + """ + + __tablename__ = "cities" + + name = Column(String(128), nullable=False) + + state_id = Column(String(60), ForeignKey("states.id"), + nullable=False) + + if os.getenv("HBNB_TYPE_STORAGE") == 'db': + places = relationship('Place', + backref='cities', + cascade='all, delete, delete-orphan') + else: + name: str = "" + state_id: str = "" From 4d8948ca74b1d8e6a3c3ca9486328f54f8ab560c Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 10:59:42 +0300 Subject: [PATCH 038/101] chore(models/city.py): Added executable rights --- models/city.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 models/city.py diff --git a/models/city.py b/models/city.py old mode 100644 new mode 100755 From 22baca0fc8488a5bf4486e02fcc777916b94aa51 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 11:24:11 +0300 Subject: [PATCH 039/101] feat(models/place.py): Updated class with class attributes for ORM mapping. Added association table for many-to-many relationship mapping between places and amenities tables. Added property and setter methods for use with FileStorage. --- models/place.py | 138 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 121 insertions(+), 17 deletions(-) diff --git a/models/place.py b/models/place.py index 5221e8210d17..e39530dbd2c6 100644 --- a/models/place.py +++ b/models/place.py @@ -1,18 +1,122 @@ #!/usr/bin/python3 -""" Place Module for HBNB project """ -from models.base_model import BaseModel - - -class Place(BaseModel): - """ A place to stay """ - city_id = "" - user_id = "" - name = "" - description = "" - number_rooms = 0 - number_bathrooms = 0 - max_guest = 0 - price_by_night = 0 - latitude = 0.0 - longitude = 0.0 - amenity_ids = [] +# -*- coding: utf-8 -*- +""" +Place Module for HBNB project. + +Defines the `Place` class, which represents accommodations available in the +system. + +Handles relationships with `City`, `User`, `Review`, and `Amenity`. +""" +import os +from models.base_model import BaseModel, Base +# from models.amenity import Amenity +# from models.review import Review +from sqlalchemy.orm import relationship +from sqlalchemy import Integer, Column, String, ForeignKey, Float, Table +from typing import List + + +# association table for the many-to-many relationship +place_amenity = Table( + "place_amenity", + Base.metadata, + Column("place_id", String(60), ForeignKey("places.id"), + nullable=False, primary_key=True), + Column("amenity_id", String(60), ForeignKey("amenities.id"), + nullable=False, primary_key=True), +) + + +class Place(BaseModel, Base): + """ + Represents a place (accommodation) in the HBNB project. + + Inherits from: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for places. + city_id (Column or str): The ID of the associated city (foreign key, + required). + user_id (Column or str): The ID of the owner/user (foreign key, + required). + name (Column or str): The name of the place (string, required). + description (Column or str): A detailed description of the place. + number_rooms (Column or int): The number of rooms in the place. + number_bathrooms (Column or int): The number of bathrooms in the place. + max_guest (Column or int): The maximum number of guests allowed. + price_by_night (Column or int): The cost per night. + latitude (Column or float): The latitude coordinate of the place. + longitude (Column or float): The longitude coordinate of the place. + reviews (relationship or property): A one-to-many relationship with + `Review` (DB storage). + amenities (relationship or property): A many-to-many relationship with + `Amenity` (DB storage). + """ + + __tablename__ = "places" + + amenity_ids: List[str] = [] + + city_id = Column(String(60), ForeignKey('cities.id'), nullable=False) + user_id = Column(String(60), ForeignKey('users.id'), nullable=False) + name = Column(String(128), nullable=False) + description = Column(String(1024), nullable=False) + number_rooms = Column(Integer, nullable=False, default=0) + number_bathrooms = Column(Integer, nullable=False, default=0) + max_guest = Column(Integer, nullable=False, default=0) + price_by_night = Column(Integer, nullable=False, default=0) + latitude = Column(Float, nullable=False, default=0.0) + longitude = Column(Float, nullable=False, default=0.0) + + if os.getenv("HBNB_TYPE_STORAGE") == 'db': + reviews = relationship('Review', + backref='place', + cascade='all, delete, delete-orphan') + + amenities = relationship('Amenity', + secondary='place_amenity', + overlaps="place_amenities", + viewonly=False) + + else: + city_id: str = "" + user_id: str = "" + name: str = "" + description: str = "" + number_rooms: int = 0 + number_bathrooms: int = 0 + max_guest: int = 0 + price_by_night: int = 0 + latitude: float = 0.0 + longitude: float = 0.0 + + @property + def reviews(self): + """Returns a list of `Review` instances related to this Place.""" + from models import storage + from models.review import Review # Avoid circular import issues + + return [review for review in storage.all( + Review).values() if review.place_id == self.id] + + @property + def amenities(self): + """Returns a list of `Amenity` instances linked to this Place.""" + from models import storage + from models.amenity import Amenity # Avoid circular import issues + + return [amenity for amenity in storage.all( + Amenity).values() if amenity.id in self.amenity_ids] + + @amenities.setter + def amenities(self, obj): + """Add an Amenity instance to this Place.""" + from models.amenity import Amenity # Avoid circular import issues + + if isinstance(obj, Amenity): + if obj.id not in self.amenity_ids: + self.amenity_ids.append(obj.id) From 7eae5aac64a79746e10443d052dccb6143e47dbb Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 11:25:09 +0300 Subject: [PATCH 040/101] chore(models/place.py): Added executable rights --- models/place.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 models/place.py diff --git a/models/place.py b/models/place.py old mode 100644 new mode 100755 From 166e957893ea3056c852c9c7d8b6176826cf70f8 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 11:33:21 +0300 Subject: [PATCH 041/101] feat(model/review.py): Updated class with attributes for SQLAlchemy table mapping --- models/review.py | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/models/review.py b/models/review.py index c487d90d34f0..8d8cdd4db975 100644 --- a/models/review.py +++ b/models/review.py @@ -1,10 +1,40 @@ #!/usr/bin/python3 -""" Review module for the HBNB project """ -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +Review module for the HBNB project. +Defines the `Review` class, which stores user reviews related to places. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy import Column, String, ForeignKey -class Review(BaseModel): - """ Review classto store review information """ - place_id = "" - user_id = "" - text = "" + +class Review(BaseModel, Base): + """ + Represents a review given by a user for a specific place. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for reviews. + place_id (Column or str): The ID of the associated place (foreign key, + required). + user_id (Column or str): The ID of the user who wrote the review + (foreign key, required). + text (Column or str): The review content (string, required). + """ + + __tablename__ = "reviews" + + place_id = Column(String(60), ForeignKey('places.id'), nullable=False, ) + user_id = Column(String(60), ForeignKey('users.id'), nullable=False) + text = Column(String(1024), nullable=False) + + if os.getenv("HBNB_TYPE_STORAGE") != 'db': + place_id: str = "" + user_id: str = "" + text: str = "" From fc6f751eb80c52373ccfb6385c70d62fafb92a78 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 11:33:21 +0300 Subject: [PATCH 042/101] feat(models/review.py): Updated class with attributes for SQLAlchemy table mapping --- models/review.py | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) mode change 100644 => 100755 models/review.py diff --git a/models/review.py b/models/review.py old mode 100644 new mode 100755 index c487d90d34f0..8d8cdd4db975 --- a/models/review.py +++ b/models/review.py @@ -1,10 +1,40 @@ #!/usr/bin/python3 -""" Review module for the HBNB project """ -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +Review module for the HBNB project. +Defines the `Review` class, which stores user reviews related to places. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy import Column, String, ForeignKey -class Review(BaseModel): - """ Review classto store review information """ - place_id = "" - user_id = "" - text = "" + +class Review(BaseModel, Base): + """ + Represents a review given by a user for a specific place. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for reviews. + place_id (Column or str): The ID of the associated place (foreign key, + required). + user_id (Column or str): The ID of the user who wrote the review + (foreign key, required). + text (Column or str): The review content (string, required). + """ + + __tablename__ = "reviews" + + place_id = Column(String(60), ForeignKey('places.id'), nullable=False, ) + user_id = Column(String(60), ForeignKey('users.id'), nullable=False) + text = Column(String(1024), nullable=False) + + if os.getenv("HBNB_TYPE_STORAGE") != 'db': + place_id: str = "" + user_id: str = "" + text: str = "" From 6725fe27d3054449d8a4d3aa5eead30129ec736a Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 11:43:44 +0300 Subject: [PATCH 043/101] feat(models/state.py): Update class with attributes for SQLAlchemy table mapping --- models/state.py | 58 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/models/state.py b/models/state.py index 583f041f07e4..1a6b6bd3bae3 100644 --- a/models/state.py +++ b/models/state.py @@ -1,8 +1,56 @@ #!/usr/bin/python3 -""" State Module for HBNB project """ -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +State Module for HBNB project. +Defines the `State` class, which represents a state in the application. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy.orm import relationship +from sqlalchemy import Column, String -class State(BaseModel): - """ State class """ - name = "" + +class State(BaseModel, Base): + """ + Represents a state in the HBNB project. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for states. + name (Column or str): The name of the state (string, required). + cities (relationship or property): A relationship to `City` objects + if using DB storage, otherwise a + property returning related `City` + instances. + """ + + __tablename__ = "states" + + name = Column(String(128), nullable=False) + + if os.getenv("HBNB_TYPE_STORAGE") == 'db': + cities = relationship('City', + backref='state', + cascade='all, delete, delete-orphan') + else: + name: str = "" + + @property + def cities(self): + """ + Returns a list of `City` instances related to this `State` + when using file storage. + + Retrieves all `City` objects from storage and filters them + based on `state_id`. + """ + from models import storage + from models.city import City + + return [city for city in storage.all( + City).values() if city.state_id == self.id] From f75d27f3627926a4552b003180ac95dc3de87a79 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 11:44:40 +0300 Subject: [PATCH 044/101] chore(models/state.py): Added executable rights --- models/state.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 models/state.py diff --git a/models/state.py b/models/state.py old mode 100644 new mode 100755 From 10f26ef4a7c1fb7b7b5572065e7dc111b32533b3 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 11:51:44 +0300 Subject: [PATCH 045/101] feat(models/user.py): Updated class with attributes for SLQAlchemy table mapping --- models/user.py | 59 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/models/user.py b/models/user.py index 4b54a6d24120..9d022292e167 100644 --- a/models/user.py +++ b/models/user.py @@ -1,11 +1,54 @@ #!/usr/bin/python3 -"""This module defines a class User""" -from models.base_model import BaseModel +# -*- coding: utf-8 -*- +""" +User Module for HBNB project. +Defines the `User` class, which represents a user in the application. +""" +import os +from models.base_model import BaseModel, Base +from sqlalchemy.orm import relationship +from sqlalchemy import Column, String -class User(BaseModel): - """This class defines a user by various attributes""" - email = '' - password = '' - first_name = '' - last_name = '' + +class User(BaseModel, Base): + """ + Represents a user in the HBNB project. + + Inherits from_: + - BaseModel: Provides common attributes like `id`, `created_at`, and + `updated_at`. + - Base: Enables SQLAlchemy ORM functionality. + + Attributes_: + __tablename__ (str): The name of the MySQL table for users. + email (Column or str): The user's email address (string, required). + password (Column or str): The user's password (string, required). + first_name (Column or str): The user's first name (string, optional). + last_name (Column or str): The user's last name (string, optional). + places (relationship or list): Relationship to `Place` objects if + using DB storage. + reviews (relationship or list): Relationship to `Review` objects if + using DB storage. + """ + + __tablename__ = "users" + + email = Column(String(128), nullable=False) + password = Column(String(128), nullable=False) + first_name = Column(String(128), nullable=False) + last_name = Column(String(128), nullable=False) + + if os.getenv("HBNB_TYPE_STORAGE") == 'db': + places = relationship('Place', + backref='user', + cascade='all, delete, delete-orphan') + + reviews = relationship('Review', + backref='user', + cascade='all, delete, delete-orphan') + else: + email = '' + password = '' + first_name = '' + last_name = '' From 9a8fc14c34e2badabe9fab18e99e20346ae39c97 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 12:35:23 +0300 Subject: [PATCH 046/101] test(tests/test_console.py): Add console tests for file and database storage --- tests/test_console.py | 1144 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1144 insertions(+) create mode 100755 tests/test_console.py diff --git a/tests/test_console.py b/tests/test_console.py new file mode 100755 index 000000000000..4f72847b4718 --- /dev/null +++ b/tests/test_console.py @@ -0,0 +1,1144 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Unittest suites for the HBNBCommand class for FileStorage and DBStorage. +""" +import os +import time +import unittest +import MySQLdb +from sqlalchemy.exc import IntegrityError +from io import StringIO +from unittest.mock import patch +from console import HBNBCommand +from models import storage +from models.amenity import Amenity +from models.city import City +from models.place import Place +from models.review import Review +from models.state import State +from models.user import User +from models.engine.db_storage import DBStorage +from tests.test_models.test_engine.test_db_storage import BaseTestDBStorage + + +__author__ = "Albert Mwanza" +__license__ = "MIT" +__date__ = "2025-02-11" +__version__ = "2.1" + + +class TestBuiltInConsoleCommands(unittest.TestCase): + """Test cases for the HBNBCommand class.""" + + def test_quit_command(self): + """Test the quit command.""" + with patch('sys.stdout', new=StringIO()) as output: + self.assertTrue(HBNBCommand().onecmd("quit")) + + def test_eof_command(self): + """Test the EOF command.""" + with patch('sys.stdout', new=StringIO()) as output: + self.assertTrue(HBNBCommand().onecmd("EOF")) + + def test_empty_line(self): + """Test empty line input.""" + with patch('sys.stdout', new=StringIO()) as output: + self.assertFalse(HBNBCommand().onecmd("")) + + def test_help_command(self): + """Test the help command.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("help") + self.assertIn("Documented commands", output.getvalue()) + + @patch('sys.stdout', new_callable=StringIO) + def test_empty_line(self, mock_stdout): + """Test that empty line does nothing.""" + # Simulate pressing ENTER with an empty line + HBNBCommand().onecmd("") + self.assertEqual(mock_stdout.getvalue(), "") # No output expected + + @patch('sys.stdout', new_callable=StringIO) + def test_empty_line_with_spaces(self, mock_stdout): + """Test that a line with only spaces does nothing.""" + # Simulate pressing ENTER with a line of spaces + HBNBCommand().onecmd(" ") + self.assertEqual(mock_stdout.getvalue(), "") # No output expected + + +@unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") == "db", + "Skipping: not using DBStorage") +class TestFileStorageConsole(unittest.TestCase): + _classes = {'BaseModel', + 'User', + 'Place', + 'State', + 'City', + 'Amenity', + 'Review' + } + + @classmethod + def setUpClass(cls): + """Set up the test environment by creating a FileStorage instance and + clearing the file.""" + cls.file_path = "file.json" + cls.storage = storage + cls.storage.reload() # Ensure the database session is reloaded + cls.storage._FileStorage__objects.clear() + + @classmethod + def tearDownClass(cls): + """Clean up after each test by removing the test file.""" + if os.path.exists(cls.file_path): + os.remove(cls.file_path) + + def setUp(self): + """Set up test environment""" + self.storage.reload() + + def tearDown(self): + """Clean up after each test""" + # Clear the in-memory cache of objects + self.storage._FileStorage__objects.clear() + + +class TestCreateCommand(TestFileStorageConsole): + """Test cases for the create command.""" + + def test_create_each_class_without_kwargs(self): + """Test create command for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + self.assertRegex(instance_id, + r'^[0-9a-f-]{36}$') + key = cls + "." + instance_id + print(key) + self.assertIn(key, self.storage.all()) + + def test_create_invalid_class(self): + """Test create with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("create FakeClass") + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_storage_entries_count(self): + """Test Number of record in storage for each class.""" + self.assertEqual(len(self.storage.all()), len(self._classes)) + + for model in self._classes: + if model != "BaseModel": + self.assertEqual(len(self.storage.all(model)), 1) + + +class TestShowCommand(TestFileStorageConsole): + """Test cases for the show command.""" + + def test_show_valid_instance(self): + """Test show with a valid instance.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + with patch('sys.stdout', new=StringIO()) as show_output: + HBNBCommand().onecmd(f"show {cls} {instance_id}") + self.assertIn(instance_id, show_output.getvalue().strip()) + + def test_show_invalid_class(self): + """Test show with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("show FakeClass 1234") + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_show_missing_id(self): + """Test show with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"show {cls}") + self.assertEqual(output.getvalue().strip(), + "** instance id missing **") + + def test_show_nonexistent_id(self): + """Test show with a nonexistent ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"show {cls} 1234") + self.assertEqual(output.getvalue().strip(), + "** no instance found **") + + +class TestUpdateCommand(TestFileStorageConsole): + """Test cases for the show command.""" + + def test_update_valid_instance(self): + """Test update command for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + with patch('sys.stdout', new=StringIO()) as update_output: + HBNBCommand().onecmd( + f"update {cls} {instance_id} author 'Albert'") + self.assertEqual(update_output.getvalue().strip(), "") + + def test_update_invalid_class(self): + """Test show with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("update FakeClass 1234 author 'Albert'") + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_update_nonexistent_id(self): + """Test show with a nonexistent ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + with patch('sys.stdout', new=StringIO()) as update_output: + HBNBCommand().onecmd(f"update {cls} 1234 author 'Albert'") + self.assertEqual(update_output.getvalue().strip(), + "** no instance found **") + + def test_update_missing_id(self): + """Test show with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"update {cls}") + self.assertEqual(output.getvalue().strip(), + "** instance id missing **") + + def test_update_missing_attr_name(self): + """Test show with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + with patch('sys.stdout', new=StringIO()) as update_output: + HBNBCommand().onecmd( + f"update {cls} {instance_id}") + self.assertEqual(update_output.getvalue().strip(), + "** attribute name missing **") + + def test_update_missing_attr_value(self): + """Test show with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + with patch('sys.stdout', new=StringIO()) as update_output: + HBNBCommand().onecmd(f"update {cls} {instance_id} author") + self.assertEqual(update_output.getvalue().strip(), + "** value missing **") + + +class TestDestroyCommand(TestFileStorageConsole): + """Test cases for the destroy command.""" + + def test_destroy_valid_instance(self): + """Test destroy with a valid instance.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + with patch('sys.stdout', new=StringIO()) as destroy_output: + HBNBCommand().onecmd(f"destroy {cls} {instance_id}") + self.assertEqual(destroy_output.getvalue().strip(), "") + + def test_destroy_invalid_class(self): + """Test destroy with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("destroy FakeClass 1234") + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_destroy_missing_id(self): + """Test destroy with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"destroy {cls}") + self.assertEqual(output.getvalue().strip(), + "** instance id missing **") + + def test_destroy_nonexistent_id(self): + """Test destroy with a nonexistent ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + with patch('sys.stdout', new=StringIO()) as destroy_output: + HBNBCommand().onecmd(f"destroy {cls} 1234") + self.assertEqual(destroy_output.getvalue().strip(), + "** no instance found **") + + +class TestAllCommand(TestFileStorageConsole): + """Test cases for the all command.""" + + def setUp(self): + """Set up the test environment by creating a FileStorage instance and + clearing the file.""" + self.storage.reload() + + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + + def test_all(self): + """Test all command without class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("all") + self.assertIsInstance(output.getvalue().strip(), str) + + def test_all_with_class(self): + """Test all command with a valid class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"all {cls}") + self.assertIsInstance(output.getvalue().strip(), str) + + def test_all_invalid_class(self): + """Test all command with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd("all FakeClass") + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + +class TestDotCommands(TestFileStorageConsole): + """Test cases for count, show, and update commands for each class.""" + + def setUp(self): + """Set up the test environment by creating a FileStorage instance and + clearing the file.""" + print("setUp: - > ", self._classes) + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + + def test_count(self): + """Test count command for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as count_output: + command = HBNBCommand().precmd( + f"{cls}.count()") # Manually process command + HBNBCommand().onecmd(command) # Execute transformed command + self.assertRegex(count_output.getvalue().strip(), r'^\d+$') + + def test_show_nonexistent(self): + """Test show command with nonexistent ID for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as show_output: + command = HBNBCommand().precmd( + f"{cls}.show(1234)") # Manually process command + HBNBCommand().onecmd(command) # Execute transformed command + self.assertEqual(show_output.getvalue().strip(), + "** no instance found **") + + def test_all(self): + """Test all command without class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + command = HBNBCommand().precmd( + f"{cls}.all()") # Manually process command + HBNBCommand().onecmd(command) + self.assertIsInstance(output.getvalue().strip(), str) + self.assertNotEqual(len(output.getvalue().strip()), len('[]')) + self.assertTrue(output.getvalue().strip().startswith("[") + and + output.getvalue().strip().endswith("]")) + + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + + all_instance_entry = f"[{cls}] ({instance_id})" + + with patch('sys.stdout', new=StringIO()) as all_output: + command = HBNBCommand().precmd( + f"{cls}.all()") # Manually process command + + # Execute transformed command + HBNBCommand().onecmd(command) + self.assertIn(all_instance_entry, + all_output.getvalue().strip()) + + # clear storage and remove json file to test for empty storage call + self.tearDown() + self.tearDownClass() + + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + command = HBNBCommand().precmd( + f"{cls}.all()") # Manually process command + HBNBCommand().onecmd(command) + + self.assertEqual(output.getvalue().strip(), '[]') + self.assertTrue(len(output.getvalue().strip()), 2) + + def test_all_invalid_class(self): + """Test all command with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + # Manually process command + command = HBNBCommand().precmd("FakeClass.all()") + HBNBCommand().onecmd(command) # Execute transformed command + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_update(self): + """Test update command for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + + with patch('sys.stdout', new=StringIO()) as update_output: + command = HBNBCommand().precmd( + f"{cls}.update({instance_id}, attr_name, 'value')") + HBNBCommand().onecmd(command) + + self.assertEqual(update_output.getvalue().strip(), "") + + with patch('sys.stdout', new=StringIO()) as all_output: + command = HBNBCommand().precmd( + f"{cls}.all()") # Manually process command + + # Execute transformed command + HBNBCommand().onecmd(command) + self.assertTrue(all(item in all_output.getvalue().strip() + for item in ('attr_name', 'value'))) + + def test_update_nonexistent(self): + """Test update command with nonexistent ID for each class.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as update_output: + command = HBNBCommand().precmd( + f"{cls}.update(1234, attr_name, 'value')") + HBNBCommand().onecmd(command) + self.assertEqual(update_output.getvalue().strip(), + "** no instance found **") + + def test_destroy_valid_instance(self): + """Test destroy with a valid instance.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + + with patch('sys.stdout', new=StringIO()) as destroy_output: + command = HBNBCommand().precmd( + f"{cls}.destroy({instance_id})") + HBNBCommand().onecmd(command) + + self.assertEqual(destroy_output.getvalue().strip(), "") + + def test_destroy_invalid_class(self): + """Test destroy with an invalid class.""" + with patch('sys.stdout', new=StringIO()) as output: + command = HBNBCommand().precmd("FakeClass.destroy(1234)") + HBNBCommand().onecmd(command) + + self.assertEqual(output.getvalue().strip(), + "** class doesn't exist **") + + def test_destroy_missing_id(self): + """Test destroy with a missing ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + command = HBNBCommand().precmd(f"{cls}.destroy()") + HBNBCommand().onecmd(command) + + self.assertEqual(output.getvalue().strip(), + "** instance id missing **") + + def test_destroy_nonexistent_id(self): + """Test destroy with a nonexistent ID.""" + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as destroy_output: + command = HBNBCommand().precmd(f"{cls}.destroy(1234)") + HBNBCommand().onecmd(command) + + self.assertEqual(destroy_output.getvalue().strip(), + "** no instance found **") + + +class TestShowUpdateCommandsWithDict(TestFileStorageConsole): + """Test cases for show and update commands with dictionary input for each + class.""" + + def test_show_with_dict(self): + """ + Test show command when a dictionary is used in update for each class. + """ + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + HBNBCommand().onecmd(f"create {cls}") + instance_id = output.getvalue().strip() + + with patch('sys.stdout', new=StringIO()) as update_output: + command = HBNBCommand().precmd( + f"{cls}.update({instance_id}, {{'author': 'Albert'}})") + HBNBCommand().onecmd(command) + self.assertEqual(update_output.getvalue().strip(), "") + + with patch('sys.stdout', new=StringIO()) as show_output: + command = HBNBCommand().precmd( + f"{cls}.show({instance_id})") + HBNBCommand().onecmd(command) + self.assertIn("'author': 'Albert'", + show_output.getvalue().strip()) + + def test_update_with_dict_nonexistent_id(self): + """ + Test update command with dictionary for nonexistent ID in each class. + """ + for cls in self._classes: + with patch('sys.stdout', new=StringIO()) as output: + command = HBNBCommand().precmd( + f"{cls}.update(1234, {{'author': 'Albert'}})") + HBNBCommand().onecmd(command) + + self.assertEqual(output.getvalue().strip(), + "** no instance found **") + + +class TestDBStorageConsole(BaseTestDBStorage): + + DB_CLASSES = {'users': User, + 'places': Place, + 'states': State, + 'cities': City, + 'amenities': Amenity, + 'reviews': Review + } + + @classmethod + def get_model_class(cls, tablename): + """Get the model class for a given table name""" + return cls.DB_CLASSES.get(tablename) + + @classmethod + def create_attr_for_entries_created_based_table_name(cls, tablename): + """Get the model class for a given table name""" + setattr(cls, tablename, []) + + def test_01_DBStorage__object_has_no_object_of_the_model(self): + """Confirm __objects is empty""" + self.assertEqual(len(self.storage.all(self.model)), 0) + + def test_02_table_is_empty(self): + """Confirm table is empty""" + self.assertEqual(self.get_table_entries_count(), 0) + + def test_04_DBStorage__objects_not_empty_and_has_one_object(self): + """Confirm __objects is not empty""" + entries = getattr(self, self.tablename) + + self.assertTrue(len(self.storage.all(self.model)) > 0) + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count()) + self.assertEqual(len(self.storage.all(self.model)), len(entries)) + self.assertEqual(len(self.storage.all(self.model)), 1) + self.assertEqual(len(entries), 1) + self.assertEqual(self.get_table_entries_count(), 1) + self.assertIn(entries[-1], self.storage.all(self.model).keys()) + print(entries) + + def test_05_table_is_not_empty_and_has_one_entry(self): + """Confirm the model's table is not empty""" + entries = getattr(self, self.tablename) + + self.assertTrue(self.get_table_entries_count() > + 0, "Entry was not added to DB") + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count()) + self.assertEqual(len(self.storage.all(self.model)), len(entries)) + self.assertEqual(self.get_table_entries_count(), len(entries)) + self.assertEqual(len(self.storage.all(self.model)), 1) + self.assertEqual(len(entries), 1) + self.assertEqual(self.get_table_entries_count(), 1) + print(entries) + + def test_07_DBStorage__objects_has_two_objects_of_the_model(self): + """Confirm __objects size is greater thant 1""" + # update table entries count + entries = getattr(self, self.tablename) + + self.assertTrue(len(self.storage.all(self.model)) + > 1, "Entry was not added to DB") + self.assertEqual(len(self.storage.all(self.model)), + len(entries), "Entry was not added to DB") + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count()) + self.assertIn(entries[-1], self.storage.all(self.model).keys()) + self.assertEqual(len(self.storage.all(self.model)), 2) + self.assertEqual(len(entries), 2) + self.assertEqual(self.get_table_entries_count(), 2) + print(entries) + + def test_08_table_has_two_entries(self): + """Confirm users table has more than one row""" + entries = getattr(self, self.tablename) + + self.assertTrue(self.get_table_entries_count() > + 1, "Entry was not added to DB") + self.assertEqual(self.get_table_entries_count(), + len(entries), "Entry was not added to DB") + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count()) + self.assertEqual(self.get_table_entries_count(), len(entries)) + self.assertEqual(len(self.storage.all(self.model)), 2) + self.assertEqual(len(entries), 2) + self.assertEqual(self.get_table_entries_count(), 2) + + def test_09_deletion_of_last_table_entry(self): + """Cleanup: Delete the last created table entry from the database""" + entries = getattr(self, self.tablename) + initial_count = len(entries) + + self.assertEqual(self.get_table_entries_count(), initial_count) + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count()) + self.assertEqual(self.get_table_entries_count(), initial_count) + self.assertEqual(len(self.storage.all(self.model)), 2) + self.assertEqual(len(entries), 2) + self.assertEqual(self.get_table_entries_count(), 2) + + key = entries[-1] + + self.storage.delete(storage.all(self.model)[key]) + + entry_id = key.split('.')[-1] + + print("entries before delete", entries) + # Refresh the MySQLdb connection to see the changes + self.conn.commit() + + entries.pop(-1) + + print("entries after delete", entries) + + self.assertTrue(len(self.users) < initial_count, + "Entry not deleted from the DB") + self.assertEqual(self.get_table_entries_count(), + initial_count-1, "Entry not deleted from the DB") + self.assertEqual(len(self.storage.all(self.model)), + self.get_table_entries_count(), + "Entry not deleted from the DB") + self.assertEqual(len(self.storage.all(self.model)), + initial_count-1, "Entry not deleted from the DB") + self.assertEqual(len(entries), + self.get_table_entries_count(), + "Entry not deleted from the DB") + self.assertEqual(len(self.storage.all(self.model)), len( + entries), "Entry not deleted from the DB") + self.assertEqual(len(self.storage.all(self.model)), 1) + self.assertEqual(len(entries), 1) + self.assertEqual(self.get_table_entries_count(), 1) + + query = f"SELECT * FROM {self.tablename} WHERE id=%s" + self.cursor.execute(query, (entry_id,)) + result = self.cursor.fetchone() + self.assertTrue(result is None) + + def test_10_only_one_entry_exists_after_last_deletion(self): + """Helper function to count records in users table""" + query = f"SELECT * FROM {self.tablename}" + self.cursor.execute(query) + result = self.cursor.fetchone() + self.assertTrue(result is not None) + + def test_11_create_a_table_entry_without_attributes_values_pairs(self): + """Test that creating a User with missing attributes via the console + does not add it to the database""" + models = {'users': "User", + 'places': "Place", + 'states': "State", + 'cities': "City", + 'amenities': "Amenity", + 'reviews': "Review" + } + + with patch("sys.stdout", new=StringIO()) as output: + with self.assertRaises(IntegrityError): + args = f'create {models[self.tablename]}' + HBNBCommand().onecmd(args) + # Roll back the session to clear the invalid state + self.storage.rollback() + + +class Test_02_User(TestDBStorageConsole): + """Tests DBStorage integration with console commands for the User model""" + tablename = "users" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_one_user_with_valid_attributes(self): + """Test that creating a User via the console adds it to the database""" + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + 'create User email="gui@hbtn.io" password="guipwd" ' + + 'first_name="Guillaume" last_name="Snow"') + user_id = output.getvalue().strip() + + self.users.append("User." + user_id) + self.assertTrue(len(user_id) > 0, + "Failed to get User ID from console output") + self.assertRegex(user_id, r'^[0-9a-f-]{36}$') + + def test_06_create_additional_user_with_valid_attributes(self): + """Test that creating a User via the console adds it to the database""" + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + 'create User email="johndoe@example.com" ' + + 'password="password" first_name="John" last_name="Doe"') + user_id = output.getvalue().strip() + + self.users.append("User." + user_id) + self.assertTrue(len(user_id) > 0, + "Failed to get User ID from console output") + self.assertRegex(user_id, r'^[0-9a-f-]{36}$') + + def test_12_create_user_with_single_attribute_value_pair(self): + """Test that creating a User with missing attributes via the console + does not add it to the database""" + # Test missing email attribute + with patch("sys.stdout", new=StringIO()) as output: + with self.assertRaises(IntegrityError): + args = 'create User first_name="Guillaume"' + HBNBCommand().onecmd(args) + # Roll back the session to clear the invalid state + self.storage.rollback() + + def test_13_create_user_with_missing_attribute_value_pairs(self): + """Test that creating a User with missing attributes via the console + does not add it to the database""" + # Test missing email attribute + with patch("sys.stdout", new=StringIO()) as output: + with self.assertRaises(IntegrityError): + args = 'create User password="guipwd" ' +\ + 'first_name="Guillaume" last_name="Snow"' + HBNBCommand().onecmd(args) + # Roll back the session to clear the invalid state + self.storage.rollback() + + # Test missing password attribute + with patch("sys.stdout", new=StringIO()) as output: + with self.assertRaises(IntegrityError): + args = 'create User email="gui@hbtn.io" ' +\ + 'first_name="Guillaume" last_name="Snow"' + HBNBCommand().onecmd(args) + # Roll back the session to clear the invalid state + self.storage.rollback() + + # Test missing first_name attribute + with patch("sys.stdout", new=StringIO()) as output: + with self.assertRaises(IntegrityError): + args = 'create User email="gui@hbtn.io" ' +\ + 'password="guipwd" last_name="Snow"' + HBNBCommand().onecmd(args) + # Roll back the session to clear the invalid state + self.storage.rollback() + + # Test missing last_name attribute + with patch("sys.stdout", new=StringIO()) as output: + with self.assertRaises(IntegrityError): + args = 'create User email="gui@hbtn.io" ' +\ + 'password="guipwd" first_name="Guillaume"' + HBNBCommand().onecmd(args) + # Roll back the session to clear the invalid state + self.storage.rollback() + + +class Test_03_State(TestDBStorageConsole): + """Tests DBStorage integration with console commands for the State model""" + tablename = "states" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_one_state_with_valid_attributes(self): + """Test that creating a User via the console adds it to the database""" + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create State name="California"') + state_id = output.getvalue().strip() + + self.states.append("State." + state_id) + self.assertTrue(len(state_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + + def test_06_create_additional_state_with_valid_attributes(self): + """Test that creating a User via the console adds it to the database""" + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create State name="Nevada"') + state_id = output.getvalue().strip() + + self.states.append("State." + state_id) + self.assertTrue(len(state_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + + +class Test_04_City(TestDBStorageConsole): + """Tests DBStorage integration with console commands for the City model""" + tablename = "cities" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_one_city_with_valid_attributes(self): + """Test that creating a City via the console adds it to the database""" + + self.assertNotEqual(self.get_table_entries_count( + "states"), 0, "states table is empty") + self.assertTrue(self.get_table_entries_count( + "states") >= 1, "states table is empty") + + # 1. Get a valid state_id from and existing state + query = "SELECT id from states;" + self.cursor.execute(query) + state_id = self.cursor.fetchone()[0] + + # Create city from an existing state + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create City name="San_Francisco" state_id="{state_id}"' + HBNBCommand().onecmd(arg) + city_id = output.getvalue().strip() + + self.cities.append("City." + city_id) + self.assertTrue(len(city_id) > 0, + "Failed to get City ID from console output") + self.assertRegex(city_id, r'^[0-9a-f-]{36}$') + + def test_06_create_additional_city_with_valid_attributes(self): + """Test that creating a City via the console adds it to the database""" + initial_state_count = self.get_table_entries_count("states") + + # 1. Create a new State first since City requires a valid state_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create State name="Nevada"') + state_id = output.getvalue().strip() + + self.assertTrue(len(state_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + + self.conn.commit() + + new_state_count = self.get_table_entries_count("states") + self.assertEqual(new_state_count, initial_state_count + + 1, "State was not added to DB") + + # 2. Create a City using the new state_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + f'create City name="Reno" state_id="{state_id}"') + city_id = output.getvalue().strip() + + self.cities.append("City." + city_id) + self.assertTrue(len(city_id) > 0, + "Failed to get City ID from console output") + self.assertRegex(city_id, r'^[0-9a-f-]{36}$') + + +class Test_05_Place(TestDBStorageConsole): + """Tests DBStorage integration with console commands for the Place model""" + tablename = "places" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_one_place_with_valid_attributes(self): + """ + Test that creating a Place via the console adds it to the database. + """ + + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + self.assertNotEqual(self.get_table_entries_count( + "cities"), 0, "cities table is empty") + self.assertTrue(self.get_table_entries_count( + "cities") >= 1, "cities table is empty") + + # 1. Get a valid city_id from an existing city (required for Place) + query = 'SELECT id from cities WHERE name=%s;' + self.cursor.execute(query, ("San Francisco",)) + city_id = self.cursor.fetchone()[0] + + # 2. Get a valid user_id from an existing user (required for Place) + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + + # 3. Create a Place using the user_id and city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="{city_id}" user_id="{user_id}"' +\ + ' name="Happy_place" description="No_description_provided"' +\ + ' number_rooms=3 number_bathrooms=1 ' +\ + 'max_guest=6 price_by_night=120 latitude=37.773972 ' +\ + 'longitude=-122.431297' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.places.append("Place." + place_id) + self.assertTrue(len(place_id) > 0, + "Failed to get Place ID from console output") + self.assertRegex(place_id, r'^[0-9a-f-]{36}$') + + def test_06_create_additional_place_with_valid_attributes(self): + """ + Test that creating a Place via the console adds it to the database. + """ + initial_state_count = self.get_table_entries_count("states") + initial_user_count = self.get_table_entries_count("users") + initial_city_count = self.get_table_entries_count("cities") + + # 1. Create a User (required for Place) + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + 'create User email="johnsmith@example.com" ' + + 'password="password" first_name="John" last_name="Smith"') + user_id = output.getvalue().strip() + + self.assertTrue(len(user_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(user_id, r'^[0-9a-f-]{36}$') + + # 2. Create a City (required for Place) + + # Create a new State first since City requires a valid state_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create State name="Georgia"') + state_id = output.getvalue().strip() + + self.assertTrue(len(state_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + + # create a new city using the state_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + f'create City name="Atlanta" state_id="{state_id}"') + city_id = output.getvalue().strip() + + self.assertTrue(len(city_id) > 0, + "Failed to get City ID from console output") + self.assertRegex(city_id, r'^[0-9a-f-]{36}$') + + self.conn.commit() + + new_state_count = self.get_table_entries_count("states") + new_user_count = self.get_table_entries_count("users") + new_city_count = self.get_table_entries_count("cities") + + self.assertEqual(new_state_count, initial_state_count + + 1, "State was not added to DB") + self.assertEqual(new_user_count, initial_user_count + + 1, "User was not added to DB") + self.assertEqual(new_city_count, initial_city_count + + 1, "City was not added to DB") + + # 3. Create a Place using the user_id and city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="{city_id}" user_id="{user_id}" ' +\ + 'name="Lovely_place" description="No_description_provided"' +\ + ' number_rooms=3 number_bathrooms=1 ' +\ + 'max_guest=6 price_by_night=120 latitude=-37.773972 ' +\ + 'longitude=122.431297' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.places.append("Place." + place_id) + self.assertTrue(len(place_id) > 0, + "Failed to get Place ID from console output") + self.assertRegex(place_id, r'^[0-9a-f-]{36}$') + + +class Test_06_Review(TestDBStorageConsole): + """ + Tests DBStorage integration with console commands for the Review model. + """ + tablename = "reviews" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_one_place_with_valid_attributes(self): + """ + Test that creating a Place via the console adds it to the database. + """ + + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + self.assertNotEqual(self.get_table_entries_count( + "places"), 0, "places table is empty") + self.assertTrue(self.get_table_entries_count( + "places") >= 1, "places table is empty") + + # 1. Get a valid place_id from an existing place (required for Review) + query = 'SELECT id from places WHERE name=%s;' + self.cursor.execute(query, ("Happy place",)) + place_id = self.cursor.fetchone()[0] + print("city_id one place", place_id) + + # 2. Get a valid user_id from an existing user (required for Place) + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + print("user_id one place", user_id) + + # 3. Create a Review using the user_id and city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review place_id="{place_id}" user_id="{user_id}"' +\ + ' text="Amazing_place,_huge_kitchen"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.reviews.append("Review." + review_id) + self.assertTrue(len(review_id) > 0, + "Failed to get Review ID from console output") + self.assertRegex(review_id, r'^[0-9a-f-]{36}$') + + def test_06_create_additional_place_with_valid_attributes(self): + """ + Test that creating a Place via the console adds it to the database. + """ + initial_state_count = self.get_table_entries_count("states") + initial_user_count = self.get_table_entries_count("users") + initial_city_count = self.get_table_entries_count("cities") + initial_place_count = self.get_table_entries_count("places") + + # 1. Create a User (required for Place) + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + 'create User email="janesmith@example.com" ' + + 'password="password" first_name="Jane" last_name="Smith"') + user_id = output.getvalue().strip() + + self.assertTrue(len(user_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(user_id, r'^[0-9a-f-]{36}$') + + # 2. Create a Place (required for Review) + + # Create a new State first since City requires a valid state_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create State name="Louisiana"') + state_id = output.getvalue().strip() + + self.assertTrue(len(state_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + + # create a new city using state_id since Place requires a valid city_id + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd( + f'create City name="New_Orleans" state_id="{state_id}"') + city_id = output.getvalue().strip() + + self.assertTrue(len(city_id) > 0, + "Failed to get City ID from console output") + self.assertRegex(city_id, r'^[0-9a-f-]{36}$') + + # Create a Place using the user_id and city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="{city_id}" user_id="{user_id}" ' +\ + 'name="Lovely_place" description="No_description_provided" ' +\ + 'number_rooms=3 number_bathrooms=1 max_guest=6 ' +\ + 'price_by_night=120 latitude=-37.773972 ' +\ + 'longitude=122.431297' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.conn.commit() + + new_state_count = self.get_table_entries_count("states") + new_user_count = self.get_table_entries_count("users") + new_city_count = self.get_table_entries_count("cities") + new_place_count = self.get_table_entries_count("places") + + self.assertEqual(new_state_count, initial_state_count + + 1, "State was not added to DB") + self.assertEqual(new_user_count, initial_user_count + + 1, "User was not added to DB") + self.assertEqual(new_city_count, initial_city_count + + 1, "City was not added to DB") + self.assertEqual(new_place_count, initial_place_count + + 1, "Place was not added to DB") + + # 3. Create a Review using the user_id and place_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review place_id="{place_id}" ' +\ + f'user_id="{user_id}" text="Amazing_place,_beautiful_lounge"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.reviews.append("Review." + review_id) + self.assertTrue(len(review_id) > 0, + "Failed to get Review ID from console output") + self.assertRegex(review_id, r'^[0-9a-f-]{36}$') + + +class Test_07_Amenity(TestDBStorageConsole): + """ + Tests DBStorage integration with console commands for the Amenity model. + """ + tablename = "amenities" + + model = TestDBStorageConsole.get_model_class(tablename) + + TestDBStorageConsole.create_attr_for_entries_created_based_table_name( + tablename) + + def test_03_create_one_amenity_with_valid_attributes(self): + """Test that creating a User via the console adds it to the database""" + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create Amenity name="Wifi"') + amenity_id = output.getvalue().strip() + + self.amenities.append("Amenity." + amenity_id) + self.assertTrue(len(amenity_id) > 0, + "Failed to get Amenity ID from console output") + self.assertRegex(amenity_id, r'^[0-9a-f-]{36}$') + + def test_06_create_additional_amenity_with_valid_attributes(self): + """Test that creating a User via the console adds it to the database""" + # Capture console output + with patch("sys.stdout", new=StringIO()) as output: + HBNBCommand().onecmd('create Amenity name="Oven"') + amenity_id = output.getvalue().strip() + + self.amenities.append("Amenity." + amenity_id) + self.assertTrue(len(amenity_id) > 0, + "Failed to get State ID from console output") + self.assertRegex(amenity_id, r'^[0-9a-f-]{36}$') + + +if __name__ == "__main__": + unittest.main() From 3dd588b24cad577669aa7a6a1dabc3bc271dd627 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 12:49:13 +0300 Subject: [PATCH 047/101] test(tests/test_models/test_engine/test_db_storage.py): Add tests for DBStorage --- .../test_engine/test_db_storage.py | 722 ++++++++++++++++++ 1 file changed, 722 insertions(+) create mode 100755 tests/test_models/test_engine/test_db_storage.py diff --git a/tests/test_models/test_engine/test_db_storage.py b/tests/test_models/test_engine/test_db_storage.py new file mode 100755 index 000000000000..d1e4c6b92dfb --- /dev/null +++ b/tests/test_models/test_engine/test_db_storage.py @@ -0,0 +1,722 @@ +#!/usr/bin/python3 +""" Module for testing database storage""" + +import os +import sys +import unittest +import MySQLdb +import sqlalchemy +from unittest.mock import patch, MagicMock +from models.user import User +from models.state import State +from models.city import City +from models.amenity import Amenity +from models.place import Place +from models.review import Review +from models import storage +from models.engine.db_storage import DBStorage +from sqlalchemy.orm import make_transient + +__author__ = "Albert Mwanza" +__license__ = "MIT" +__date__ = "2025-02-12" +__version__ = "2.1" + +@unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") != "db", + "Skipping: not using DBStorage") +class BaseTestDBStorage(unittest.TestCase): + """Test cases for the DBStorage class""" + + @classmethod + def setUpClass(cls): + """Set up database connection""" + cls.storage = storage + cls.storage.reload() # Ensure the database session is reloaded + cls.storage._DBStorage__objects.clear() + + try: + # Setup MySQL connection using real credentials + cls.conn = MySQLdb.connect( + host=os.getenv("HBNB_MYSQL_HOST"), + user=os.getenv("HBNB_MYSQL_USER"), + password=os.getenv("HBNB_MYSQL_PWD"), + database=os.getenv("HBNB_MYSQL_DB"), + charset='utf8', + port=3306) + + except MySQLdb.OperationalError as e: + print(str(e).strip('()').split(',')[-1].strip()) + sys.exit(1) + + else: + cls.cursor = cls.conn.cursor() + + @classmethod + def tearDownClass(cls): + """Close database connection""" + cls.cursor.close() + cls.conn.close() + cls.storage.close() + + def setUp(self): + """Set up test environment""" + if not hasattr(self, 'tablename') or\ + self.tablename is None or\ + self.model is None: + self.skipTest("Skipping test since it has no table name") + + # Clear the in-memory cache of objects + self.storage._DBStorage__objects.clear() + + def tearDown(self): + """Clean up after each test""" + self.conn.commit() + + def test_01_type_objects(self): + """ Confirm __objects is a dict """ + self.assertTrue(type(self.storage.all()) is dict) + + def test_03_storage_var_created(self): + """ DBStorage object storage created """ + self.assertIsInstance(self.storage, DBStorage) + + def get_table_entries_count(self, table=None): + """Helper function to count records in a table""" + allowed_tables = ('users', + 'places', + 'states', + 'cities', + 'amenities', + 'reviews') + + if table is not None and table in allowed_tables: + query = f"SELECT COUNT(*) FROM {table}" + else: + query = f"SELECT COUNT(*) FROM {self.tablename}" + + self.cursor.execute(query) + return self.cursor.fetchone()[0] + + +class Test_01_TablesAreEmptyTestEnvironment(BaseTestDBStorage): + """Confirm the tables are empty i.e storage instance __objects is empty + when a storage instance is created and the test environment is 'test'.""" + + def setUp(self): + """Set up test environment""" + if os.getenv("HBNB_ENV") != "test": + self.skipTest( + "Skipping test since development environ is not test") + + self.storage._DBStorage__objects.clear() + + @patch.dict(os.environ, {"HBNB_ENV": "test"}) + # Mock DBStorage.__init__ + @patch('models.engine.db_storage.DBStorage.__init__', return_value=None) + def test_02_empty(self, mock_db_storage_init): + """Confirm __objects is empty when HBNB_ENV is 'test'.""" + # Create a mock DBStorage instance + mock_storage = DBStorage() + + # Ensure __objects is empty + self.assertEqual(len(mock_storage._DBStorage__objects), 0) + + # Verify that __init__ was called (optional) + mock_db_storage_init.assert_called_once() + + +class BaseTableTests(BaseTestDBStorage): + model = None + + new_record = None + + def create_a_new_record_instance(self, **kwargs): + if kwargs: + self.new_record = self.model(**kwargs) + else: + self.new_record = self.model() + + def get_object_record_from_db_by_id(self, obj=None, table=None): + """Helper function to count records in a table""" + tables = ('users', + 'places', + 'states', + 'cities', + 'amenities', + 'reviews') + + if table is not None and table in tables: + query = f"SELECT * FROM {table} where id=%s" + else: + query = f"SELECT * FROM {self.tablename} where id=%s" + + if obj is not None: + self.cursor.execute(query, (obj.id,)) + + return self.cursor.fetchone() + + def assert_object_in_db(self, obj): + """ + Helper method to assert that a record instance object exists in the + database. + """ + self.conn.commit() + + # Query the database directly to check if the object exists + result = self.get_object_record_from_db_by_id(obj) + + self.assertIsNotNone(result, f"Object {obj} not found in the database") + self.assertTrue(self.get_table_entries_count() >= 1) + return result + + def assert_object_not_in_db(self, obj): + """ + Helper method to assert that a record instance object exists in the + database. + """ + + self.conn.commit() + + # Query the database directly to check if the object exists + result = self.get_object_record_from_db_by_id(obj) + + self.assertIsNone( + result, + f"{self.new_record} object was not deleted from the database") + self.assertNotIn(obj, self.storage.all().values()) + + def update_record(self, **kwargs): + """Update a new_record instance with provided kwargs""" + for attr, value in kwargs.items(): + setattr(self.new_record, attr, value) + + def test_02_record_creation_without_kwargs(self): + """Test creating and saving a new_record object without kwargs to +the database""" + self.create_a_new_record_instance() + + with self.assertRaises(sqlalchemy.exc.IntegrityError): + self.new_record.save() + + # Roll back the session to clear the invalid state + self.storage.rollback() + + def test_04_table_creation_valid_kwargs(self): + """Test creating and saving a new_record object to the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + def test_05_new_record_attributes_property(self): + # Verify the object is in the database + + new_record_attr = [attr + for attr, val in self.new_record.__dict__.items() + if not attr.startswith("_") + and attr not in ("created_at", "updated_at") + and val != [] + and not isinstance(val, (User, + Place, + City, + Review, + Amenity, + State)) + ] + + result = self.assert_object_in_db(self.new_record) + + if self.model == Amenity: + try: + del new_record_attr[new_record_attr.index("state_id")] + except ValueError as e: + pass + + for attr in new_record_attr: + self.assertIn(getattr(self.new_record, attr), result) + + def test_06_record_deletion_from_db(self): + """Test deleting a new_record object from the database""" + initial_count = self.get_table_entries_count() + + self.assertTrue(self.get_table_entries_count() >= 1) + + # Delete new_record object and verify it is removed from the database + self.storage.delete(self.new_record) + + self.conn.commit() + + self.assert_object_not_in_db(self.new_record) + + new_count = self.get_table_entries_count() + self.assertTrue(new_count < initial_count) + + # Make the object transient + make_transient(self.new_record) + + +class Test_08_User(BaseTableTests): + """Test cases for the User model""" + tablename = "users" + + model = User + + new_record = User( + email="test@example1.com", + password="password", + first_name="Richard", + last_name="Quest", + ) + + def test_07_user_update(self): + """Test updating a User object in the database""" +# + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.email, "test@example1.com") + self.assertEqual(self.new_record.first_name, "Richard") + + self.test_05_new_record_attributes_property() + + # Update the user's email and name + self.update_record(first_name="Jane", email="test@example2.com") + + self.assertEqual(self.new_record.email, "test@example2.com") + self.assertEqual(self.new_record.first_name, "Jane") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE first_name=%s" + + self.cursor.execute(query, ("Jane",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "User not updated") + + def test_08_user_creation_missing_kwargs(self): + new_user1 = self.model(email="johndoe@example.com", + first_name="John", + last_name="Doe") + + new_user2 = self.model(password="password", + first_name="John", + last_name="Doe") + + new_user3 = self.model(email="bond@doubleoseven.com", + password="password", + last_name="bond") + + new_user4 = self.model(email="test@example.com", + password="password", + first_name="John") + + for user in (new_user1, new_user2, new_user3, new_user4): + with self.assertRaises(sqlalchemy.exc.IntegrityError): + user.save() + # Roll back the session to clear the invalid state + self.storage.rollback() + + +class Test_09_State(BaseTableTests): + """Test cases for the State model""" + tablename = "states" + + model = State + + new_record = State(name="Texas") + + def test_07_state_update(self): + """Test updating a User object in the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.name, "Texas") + + self.test_05_new_record_attributes_property() + + # Update the state's name + self.update_record(name="Illinois") + + self.assertEqual(self.new_record.name, "Illinois") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE name=%s" + + self.cursor.execute(query, ("Illinois",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "State not updated") + + +class Test_10_City(BaseTableTests): + """Test cases for the City model""" + tablename = "cities" + + model = City + + new_state = State(name="Massachusetts") + + new_record = City(name="Boston", state_id=new_state.id) + updated_record = None + + def test_04_table_creation_valid_kwargs(self): + """Test creating and saving a new_record object to the database""" + self.storage.new(self.new_state) + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + def test_07_city_update(self): + """Test updating a User object in the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.name, "Boston") + + self.test_05_new_record_attributes_property() + + # Update the city's name + self.update_record(name="Amherst") + self.assertEqual(self.new_record.name, "Amherst") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE name=%s" + + self.cursor.execute(query, ("Amherst",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "City not updated") + + setattr(self, 'updated_record', self.new_record) + + def test_08_city_state_relationship(self): + """Test the relationship between City and State""" + state_id = self.new_state.id + + # Verify the relationship in the database + query = """SELECT cities.name, cities.id +FROM cities +JOIN states ON cities.state_id=states.id +WHERE states.id=%s;""" + self.cursor.execute(query, (state_id,)) + + result = self.cursor.fetchone() + + city_name, city_id = result + + self.assertEqual(self.new_record.name, city_name) + self.assertEqual(self.new_record.id, city_id) + + +class Test_11_Place(BaseTableTests): + """Test cases for the Place model""" + tablename = "places" + + model = Place + + new_user = User( + email="therock@wwe.ent", + password="therock", + first_name="Dwane", + last_name="Johnson", + ) + + new_state = State(name="Hawaii") + + new_city = City(name="Holualoa", state_id=new_state.id) + + new_record = Place(name="Grand Vacation Club", + city_id=new_city.id, + user_id=new_user.id, + description="Hilton Grand Vacations Club Kings" + + "Land Waikoloa") + + def test_04_table_creation_valid_kwargs(self): + """Test creating and saving a new_record object to the database""" + self.storage.new(self.new_user) + self.storage.new(self.new_state) + self.storage.new(self.new_city) + self.storage.save() + + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + def test_07_place_update(self): + """Test updating a User object in the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.name, "Grand Vacation Club") + + self.test_05_new_record_attributes_property() + + # Update the state's name + self.update_record(name="Sheraton", + description="Sheraton Waikiki Beach Resort") + + self.assertEqual(self.new_record.name, "Sheraton") + self.assertEqual(self.new_record.description, + "Sheraton Waikiki Beach Resort") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE name=%s" + + self.cursor.execute(query, ("Sheraton",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "Place not updated") + + def test_08_place_city_user_relationship(self): + """Test the relationship between City, User and State""" + city_id = self.new_city.id + user_id = self.new_user.id + + # Verify the relationship in the database + query = """SELECT places.name, places.id +FROM places +JOIN cities ON places.city_id=cities.id +JOIN users ON places.user_id=users.id +WHERE cities.id=%s +AND users.id=%s;""" + self.cursor.execute(query, (city_id, user_id,)) + + result = self.cursor.fetchone() + + place_name, place_id = result + + self.assertEqual(self.new_record.name, place_name) + self.assertEqual(self.new_record.id, place_id) + + +class Test_12_Review(BaseTableTests): + """Test cases for the Review model""" + tablename = "reviews" + + model = Review + + new_user = User( + email="fred@futuristic.net", + password="theflintstones", + first_name="Fred", + last_name="Flintstone", + ) + + new_state = State(name="Florida") + + new_city = City(name="Orlando", state_id=new_state.id) + + new_place = Place(name="Futuristic Resorts", + city_id=new_city.id, + user_id=new_user.id, + description="Description not provided") + + new_record = Review(place_id=new_place.id, + user_id=new_user.id, + text="This is a great place!") + + def test_04_table_creation_valid_kwargs(self): + """Test creating and saving a new_record object to the database""" + self.storage.new(self.new_user) + self.storage.new(self.new_state) + self.storage.new(self.new_city) + self.storage.new(self.new_place) + self.storage.save() + + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + def test_07_review_update(self): + """Test updating a User object in the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.text, "This is a great place!") + + self.test_05_new_record_attributes_property() + + # Update the state's name + self.update_record(text="This place is amazing!") + + self.assertEqual(self.new_record.text, "This place is amazing!") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE text=%s" + + self.cursor.execute(query, ("This place is amazing!",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "Review not updated") + + def test_08_review_place_user_relationship(self): + """Test the relationship between City, User and State""" + place_id = self.new_place.id + user_id = self.new_user.id + + # Verify the relationship in the database + query = """SELECT reviews.text, reviews.id +FROM reviews +JOIN places ON reviews.place_id=places.id +JOIN users ON reviews.user_id=users.id +WHERE places.id=%s +AND users.id=%s;""" + self.cursor.execute(query, (place_id, user_id,)) + + result = self.cursor.fetchone() + + review, review_id = result + + self.assertEqual(self.new_record.text, review) + self.assertEqual(self.new_record.id, review_id) + + +class Test_13_Amenity(BaseTableTests): + """Test cases for the Amenity model""" + tablename = "amenities" + + model = Amenity + + new_user = User( + email="jj@spageage.com", + password="theflintstones", + first_name="Judy", + last_name="Jetson", + ) + + new_state = State(name="Michigan") + + new_city = City(name="Detroit", state_id=new_state.id) + + new_place = Place(name="Space Age World", + city_id=new_city.id, + user_id=new_user.id, + description="A futuristic place") + + new_review = Review(place_id=new_place.id, + user_id=new_user.id, + text="This is a fantastic place!") + + new_record = Amenity(name="Wifi") + + def test_04_table_creation_valid_kwargs(self): + """Test creating and saving a new_record object to the database""" + self.storage.new(self.new_user) + self.storage.new(self.new_state) + self.storage.new(self.new_city) + self.storage.new(self.new_place) + self.storage.new(self.new_review) + self.storage.save() + + self.new_record.state_id = self.new_state.id + + self.storage.new(self.new_record) + self.storage.save() + self.new_record.save() + + self.assert_object_in_db(self.new_record) + + def test_07_amenity_update(self): + """Test updating a User object in the database""" + self.storage.new(self.new_record) + self.storage.save() + + self.assert_object_in_db(self.new_record) + + self.assertEqual(self.new_record.name, "Wifi") + + self.test_05_new_record_attributes_property() + + # Update the state's name + self.update_record(name="Cable") + + self.assertEqual(self.new_record.name, "Cable") + + self.storage.save() + + # Verify the changes in the database + self.assert_object_in_db(self.new_record) + + self.test_05_new_record_attributes_property() + + query = F"SELECT * from {self.tablename} WHERE name=%s" + + self.cursor.execute(query, ("Cable",)) + + result = self.cursor.fetchone() + + self.assertIsNotNone(result, "Amenity not updated") + + def test_08_place_amenities_relationship(self): + """Test the relationship between City, User and State""" + place_id = self.new_place.id + self.new_place.amenities.append(self.new_record) + + self.storage.save() + + # Verify the relationship in the database + query = """SELECT amenities.name, amenities.id +FROM amenities +JOIN place_amenity ON place_amenity.amenity_id=amenities.id +JOIN places ON place_amenity.place_id=places.id +WHERE places.id=%s;""" + self.cursor.execute(query, (place_id,)) + + result = self.cursor.fetchone() + + amenity, amenity_id = result + + self.assertEqual(self.new_record.name, amenity) + self.assertEqual(self.new_record.id, amenity_id) + + +if __name__ == '__main__': + unittest.main() From c0ce84b68d867f7b6d92a8a06109d35ab8cdda2b Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 12:50:59 +0300 Subject: [PATCH 048/101] style(tests/test_models/test_engine/test_db_storage.py): pycodestyle formatting --- tests/test_models/test_engine/test_db_storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_models/test_engine/test_db_storage.py b/tests/test_models/test_engine/test_db_storage.py index d1e4c6b92dfb..e6badee9f2fe 100755 --- a/tests/test_models/test_engine/test_db_storage.py +++ b/tests/test_models/test_engine/test_db_storage.py @@ -22,6 +22,7 @@ __date__ = "2025-02-12" __version__ = "2.1" + @unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") != "db", "Skipping: not using DBStorage") class BaseTestDBStorage(unittest.TestCase): From d3a15408bcfc2cdcba93efe8f9cc244ee9c9d453 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 12:53:21 +0300 Subject: [PATCH 049/101] refactor (tests/test_models/base_model.py): Refactored setUP method to remove present json file --- tests/test_models/test_base_model.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_models/test_base_model.py b/tests/test_models/test_base_model.py index b6fef535c595..175bb23fd0bc 100755 --- a/tests/test_models/test_base_model.py +++ b/tests/test_models/test_base_model.py @@ -1,13 +1,16 @@ #!/usr/bin/python3 """ """ -from models.base_model import BaseModel +import os import unittest -import datetime -from uuid import UUID import json -import os +import time +from uuid import UUID +from datetime import datetime, timedelta +from unittest.mock import patch +from models.base_model import BaseModel +@unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") == "db", "Skipping: not using DBStorage") class test_basemodel(unittest.TestCase): """ """ @@ -19,7 +22,8 @@ def __init__(self, *args, **kwargs): def setUp(self): """ """ - pass + if os.path.exists("file.json"): + os.remove('file.json') def tearDown(self): try: From 164b7a874e2e9669264087cc151cb9047ce266ac Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 12:57:57 +0300 Subject: [PATCH 050/101] refactor (tests/test_models/base_model.py): Refactored test_str to delete _sa_instance_state attribute before test for equality of string representation of instance. --- tests/test_models/test_base_model.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_models/test_base_model.py b/tests/test_models/test_base_model.py index 175bb23fd0bc..1766238a390a 100755 --- a/tests/test_models/test_base_model.py +++ b/tests/test_models/test_base_model.py @@ -63,6 +63,12 @@ def test_save(self): def test_str(self): """ """ i = self.value() + + try: + delattr(i, '_sa_instance_state') + except AttributeError: + pass + self.assertEqual(str(i), '[{}] ({}) {}'.format(self.name, i.id, i.__dict__)) From 87aad3035bee7cdfdb701f3f546b8e75cf9c33ff Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 13:01:40 +0300 Subject: [PATCH 051/101] refactor (tests/test_models/base_model.py): Refactored test_kwargs_one. Removed improper error assertion tests --- tests/test_models/test_base_model.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_models/test_base_model.py b/tests/test_models/test_base_model.py index 1766238a390a..8d358c89fc7a 100755 --- a/tests/test_models/test_base_model.py +++ b/tests/test_models/test_base_model.py @@ -63,12 +63,12 @@ def test_save(self): def test_str(self): """ """ i = self.value() - + try: delattr(i, '_sa_instance_state') except AttributeError: pass - + self.assertEqual(str(i), '[{}] ({}) {}'.format(self.name, i.id, i.__dict__)) @@ -87,8 +87,10 @@ def test_kwargs_none(self): def test_kwargs_one(self): """ """ n = {'Name': 'test'} - with self.assertRaises(KeyError): - new = self.value(**n) + + new = self.value(**n) + self.assertIn('Name', dir(new)) + self.assertEqual(getattr(new, 'Name'), 'test') def test_id(self): """ """ From 34c77a56051c7e08bad217c14ffc3c2925ea3522 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 13:06:32 +0300 Subject: [PATCH 052/101] refactor (tests/test_models/base_model.py): Refactored test_created_at to use assertIsInstance --- tests/test_models/test_base_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_models/test_base_model.py b/tests/test_models/test_base_model.py index 8d358c89fc7a..301a6546bc34 100755 --- a/tests/test_models/test_base_model.py +++ b/tests/test_models/test_base_model.py @@ -87,7 +87,7 @@ def test_kwargs_none(self): def test_kwargs_one(self): """ """ n = {'Name': 'test'} - + new = self.value(**n) self.assertIn('Name', dir(new)) self.assertEqual(getattr(new, 'Name'), 'test') @@ -100,7 +100,7 @@ def test_id(self): def test_created_at(self): """ """ new = self.value() - self.assertEqual(type(new.created_at), datetime.datetime) + self.assertIsInstance(new.updated_at, datetime) def test_updated_at(self): """ """ From fc61bf2630e6e2e2202f9e02ab56f30c6cd4a888 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 13:12:22 +0300 Subject: [PATCH 053/101] refactor (tests/test_models/base_model.py): Refactored test_updated_at to to simulate time lapse to test the value of the attribute after an instance save. --- tests/test_models/test_base_model.py | 47 ++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/tests/test_models/test_base_model.py b/tests/test_models/test_base_model.py index 301a6546bc34..b13641f4a636 100755 --- a/tests/test_models/test_base_model.py +++ b/tests/test_models/test_base_model.py @@ -3,14 +3,13 @@ import os import unittest import json -import time -from uuid import UUID from datetime import datetime, timedelta from unittest.mock import patch from models.base_model import BaseModel -@unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") == "db", "Skipping: not using DBStorage") +@unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") == "db", + "Skipping: not using DBStorage") class test_basemodel(unittest.TestCase): """ """ @@ -28,7 +27,7 @@ def setUp(self): def tearDown(self): try: os.remove('file.json') - except: + except FileNotFoundError as e: pass def test_default(self): @@ -102,10 +101,40 @@ def test_created_at(self): new = self.value() self.assertIsInstance(new.updated_at, datetime) - def test_updated_at(self): - """ """ + @patch('models.base_model.datetime') + def test_updated_at(self, mock_datetime): + """Test that updated_at is updated correctly""" + # Configure the mock to return a specific datetime object + mock_now = datetime.now() + mock_datetime.now.return_value = mock_now + + # Create a new BaseModel instance new = self.value() - self.assertEqual(type(new.updated_at), datetime.datetime) + + # Check that updated_at is a datetime object + self.assertIsInstance(new.updated_at, datetime) + + # Check that created_at and updated_at are initially the same + self.assertEqual(new.created_at, new.updated_at) + + # Save the old updated_at value + old_updated_at = new.updated_at + + # Simulate 5 seconds later + mock_datetime.now.return_value = old_updated_at + timedelta(seconds=5) + new.save() + + # Convert the instance to a dictionary and back to a BaseModel instance n = new.to_dict() - new = BaseModel(**n) - self.assertFalse(new.created_at == new.updated_at) + + # Temporarily un-mock datetime to allow datetime.strptime + # to work correctly + with patch('models.base_model.datetime', + wraps=datetime) as mock_datetime_unmocked: + new = BaseModel(**n) + + # Check that created_at and updated_at are no longer the same + self.assertNotEqual(new.created_at, new.updated_at) + + # Check that updated_at is greater than the old updated_at + self.assertTrue(new.updated_at > old_updated_at) From 83d8a3dc96df6b062f4152d8fa34d39d526aa7f5 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 13:19:34 +0300 Subject: [PATCH 054/101] style(tests/test_models/base_model.py): renamed class name to conform to PascalCase --- tests/test_models/test_base_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_models/test_base_model.py b/tests/test_models/test_base_model.py index b13641f4a636..b0c65dcd528c 100755 --- a/tests/test_models/test_base_model.py +++ b/tests/test_models/test_base_model.py @@ -10,7 +10,7 @@ @unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") == "db", "Skipping: not using DBStorage") -class test_basemodel(unittest.TestCase): +class TestBaseModel(unittest.TestCase): """ """ def __init__(self, *args, **kwargs): @@ -25,6 +25,7 @@ def setUp(self): os.remove('file.json') def tearDown(self): + """Clean up the test file after each test.""" try: os.remove('file.json') except FileNotFoundError as e: From 1ed07bacf056b39185a93cdb670e87ed511328b8 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 13:32:23 +0300 Subject: [PATCH 055/101] docs(tests/test_models/base_model.py): Added module and method docstrings --- tests/test_models/test_base_model.py | 34 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/tests/test_models/test_base_model.py b/tests/test_models/test_base_model.py index b0c65dcd528c..a9ebea4954e2 100755 --- a/tests/test_models/test_base_model.py +++ b/tests/test_models/test_base_model.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 -""" """ +# -*- coding: utf-8 -*- +"""Unit tests for BaseModel""" import os import unittest import json @@ -11,16 +12,16 @@ @unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") == "db", "Skipping: not using DBStorage") class TestBaseModel(unittest.TestCase): - """ """ + """Test cases for the BaseModel class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test cases""" super().__init__(*args, **kwargs) self.name = 'BaseModel' self.value = BaseModel def setUp(self): - """ """ + """Set up test environment by removing file.json""" if os.path.exists("file.json"): os.remove('file.json') @@ -32,19 +33,19 @@ def tearDown(self): pass def test_default(self): - """ """ + """Test if a new instance is correctly created""" i = self.value() self.assertEqual(type(i), self.value) def test_kwargs(self): - """ """ + """Test instantiation with **kwargs""" i = self.value() copy = i.to_dict() new = BaseModel(**copy) self.assertFalse(new is i) def test_kwargs_int(self): - """ """ + """Test passing an integer in kwargs""" i = self.value() copy = i.to_dict() copy.update({1: 2}) @@ -52,16 +53,21 @@ def test_kwargs_int(self): new = BaseModel(**copy) def test_save(self): - """ Testing save """ + """Test the save method to ensure correct JSON serialization""" i = self.value() i.save() key = self.name + "." + i.id + + self.assertTrue(os.path.exists("file.json"), + "file.json was not created") + with open('file.json', 'r') as f: j = json.load(f) + self.assertIn(key, j) self.assertEqual(j[key], i.to_dict()) def test_str(self): - """ """ + """Test the string representation of an instance""" i = self.value() try: @@ -73,19 +79,19 @@ def test_str(self): i.__dict__)) def test_todict(self): - """ """ + """Test conversion to dictionary""" i = self.value() n = i.to_dict() self.assertEqual(i.to_dict(), n) def test_kwargs_none(self): - """ """ + """Test passing None as kwargs""" n = {None: None} with self.assertRaises(TypeError): new = self.value(**n) def test_kwargs_one(self): - """ """ + """Test instantiation with a single named attribute""" n = {'Name': 'test'} new = self.value(**n) @@ -93,12 +99,12 @@ def test_kwargs_one(self): self.assertEqual(getattr(new, 'Name'), 'test') def test_id(self): - """ """ + """Test that id is a string""" new = self.value() self.assertEqual(type(new.id), str) def test_created_at(self): - """ """ + """Test that created_at is a datetime object""" new = self.value() self.assertIsInstance(new.updated_at, datetime) From 62ff0d2a1a4219a361f204e1fa806e3ac7766c46 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 13:35:43 +0300 Subject: [PATCH 056/101] style(tests/test_models/amenity.py): Renamed imported class test_base_model to pascal case TestBaseModel --- tests/test_models/test_amenity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_models/test_amenity.py b/tests/test_models/test_amenity.py index e47ab0d2e09a..e7defd0cd057 100755 --- a/tests/test_models/test_amenity.py +++ b/tests/test_models/test_amenity.py @@ -1,10 +1,10 @@ #!/usr/bin/python3 """ """ -from tests.test_models.test_base_model import test_basemodel +from tests.test_models.test_base_model import TestBaseModel from models.amenity import Amenity -class test_Amenity(test_basemodel): +class test_Amenity(TestBaseModel): """ """ def __init__(self, *args, **kwargs): From fa5dc8e8338536c2623f82a1857304dedf673fc2 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 13:37:43 +0300 Subject: [PATCH 057/101] style(tests/test_models/city.py): Renamed imported class test_base_model to pascal case TestBaseModel --- tests/test_models/test_city.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_models/test_city.py b/tests/test_models/test_city.py index 2673225808c0..e8ae487b385b 100755 --- a/tests/test_models/test_city.py +++ b/tests/test_models/test_city.py @@ -1,10 +1,10 @@ #!/usr/bin/python3 """ """ -from tests.test_models.test_base_model import test_basemodel +from tests.test_models.test_base_model import TestBaseModel from models.city import City -class test_City(test_basemodel): +class test_City(TestBaseModel): """ """ def __init__(self, *args, **kwargs): From d70a4eb79eade9215850babf3bff2c95d75517c7 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 13:45:44 +0300 Subject: [PATCH 058/101] docs(tests/test_models/amenity.py): Added docstrings --- tests/test_models/test_amenity.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_models/test_amenity.py b/tests/test_models/test_amenity.py index e7defd0cd057..c40ae2ac73c1 100755 --- a/tests/test_models/test_amenity.py +++ b/tests/test_models/test_amenity.py @@ -1,19 +1,21 @@ #!/usr/bin/python3 -""" """ +"""Unit tests for Amenity""" from tests.test_models.test_base_model import TestBaseModel from models.amenity import Amenity class test_Amenity(TestBaseModel): - """ """ + """Test cases for the Amenity class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "Amenity" self.value = Amenity def test_name2(self): - """ """ + """Test that name is a string""" new = self.value() self.assertEqual(type(new.name), str) + self.assertTrue(hasattr(new, "name")) + self.assertEqual(new.name, "") From de81e99c9d5f328cb40918888ab70102f79f889c Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 14:01:11 +0300 Subject: [PATCH 059/101] docs(tests/test_models/city.py): Added docstrings --- tests/test_models/test_city.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/test_models/test_city.py b/tests/test_models/test_city.py index e8ae487b385b..699432e70c47 100755 --- a/tests/test_models/test_city.py +++ b/tests/test_models/test_city.py @@ -1,24 +1,32 @@ #!/usr/bin/python3 -""" """ +"""Unit tests for City""" from tests.test_models.test_base_model import TestBaseModel from models.city import City -class test_City(TestBaseModel): - """ """ +class TestCity(TestBaseModel): + """Test cases for the City class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "City" self.value = City def test_state_id(self): - """ """ + """ + Test that state_id exists, is a string, and defaults to an empty string + """ new = self.value() - self.assertEqual(type(new.state_id), str) + self.assertTrue(hasattr(new, "state_id")) + self.assertIsInstance(new.state_id, str) + self.assertEqual(new.state_id, "") def test_name(self): - """ """ + """ + Test that name exists, is a string, and defaults to an empty string + """ new = self.value() - self.assertEqual(type(new.name), str) + self.assertTrue(hasattr(new, "name")) + self.assertIsInstance(new.name, str) + self.assertEqual(new.name, "") From fa5a2aa6dc60742e972ba4e7647f04cf1afa1ffd Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 14:02:46 +0300 Subject: [PATCH 060/101] style(tests/test_models/amenity.py): Renamed class to Pascal naming style --- tests/test_models/test_amenity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_models/test_amenity.py b/tests/test_models/test_amenity.py index c40ae2ac73c1..62353ac910b0 100755 --- a/tests/test_models/test_amenity.py +++ b/tests/test_models/test_amenity.py @@ -4,7 +4,7 @@ from models.amenity import Amenity -class test_Amenity(TestBaseModel): +class TestAmenity(TestBaseModel): """Test cases for the Amenity class""" def __init__(self, *args, **kwargs): From f843c72766e71b5105d2355825a056c479ecefaa Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 14:23:07 +0300 Subject: [PATCH 061/101] refactor(tests/test_models/place.py): Added instantiation of place to setUp method and refactored the test methods --- tests/test_models/test_place.py | 92 +++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/tests/test_models/test_place.py b/tests/test_models/test_place.py index ec133d104ef5..048021054172 100755 --- a/tests/test_models/test_place.py +++ b/tests/test_models/test_place.py @@ -1,69 +1,85 @@ #!/usr/bin/python3 -""" """ -from tests.test_models.test_base_model import test_basemodel +# -*- coding: utf-8 -*- +"""Unit tests for Place""" +from tests.test_models.test_base_model import TestBaseModel from models.place import Place -class test_Place(test_basemodel): - """ """ +class TestPlace(TestBaseModel): + """Test cases for the Place class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "Place" self.value = Place + def setUp(self): + """Set up test environment""" + self.place = self.value() # Create a Place instance for testing + def test_city_id(self): - """ """ - new = self.value() - self.assertEqual(type(new.city_id), str) + """Test that city_id exists and is a string""" + self.assertTrue(hasattr(self.place, "city_id")) + self.assertIsInstance(self.place.city_id, str) + self.assertEqual(self.place.city_id, "") # Assuming default is empty def test_user_id(self): - """ """ - new = self.value() - self.assertEqual(type(new.user_id), str) + """Test that user_id exists and is a string""" + self.assertTrue(hasattr(self.place, "user_id")) + self.assertIsInstance(self.place.user_id, str) + self.assertEqual(self.place.user_id, "") def test_name(self): - """ """ - new = self.value() - self.assertEqual(type(new.name), str) + """Test that name exists and is a string""" + self.assertTrue(hasattr(self.place, "name")) + self.assertIsInstance(self.place.name, str) + self.assertEqual(self.place.name, "") def test_description(self): - """ """ - new = self.value() - self.assertEqual(type(new.description), str) + """Test that description exists and is a string""" + self.assertTrue(hasattr(self.place, "description")) + self.assertIsInstance(self.place.description, str) + self.assertEqual(self.place.description, "") def test_number_rooms(self): - """ """ - new = self.value() - self.assertEqual(type(new.number_rooms), int) + """Test that number_rooms exists and is an integer""" + self.assertTrue(hasattr(self.place, "number_rooms")) + self.assertIsInstance(self.place.number_rooms, int) + self.assertEqual(self.place.number_rooms, 0) def test_number_bathrooms(self): - """ """ - new = self.value() - self.assertEqual(type(new.number_bathrooms), int) + """Test that number_bathrooms exists and is an integer""" + self.assertTrue(hasattr(self.place, "number_bathrooms")) + self.assertIsInstance(self.place.number_bathrooms, int) + self.assertEqual(self.place.number_bathrooms, 0) def test_max_guest(self): - """ """ - new = self.value() - self.assertEqual(type(new.max_guest), int) + """Test that max_guest exists and is an integer""" + self.assertTrue(hasattr(self.place, "max_guest")) + self.assertIsInstance(self.place.max_guest, int) + self.assertEqual(self.place.max_guest, 0) def test_price_by_night(self): - """ """ - new = self.value() - self.assertEqual(type(new.price_by_night), int) + """Test that price_by_night exists and is an integer""" + self.assertTrue(hasattr(self.place, "price_by_night")) + self.assertIsInstance(self.place.price_by_night, int) + self.assertEqual(self.place.price_by_night, 0) def test_latitude(self): - """ """ - new = self.value() - self.assertEqual(type(new.latitude), float) + """Test that latitude exists and is a float""" + self.assertTrue(hasattr(self.place, "latitude")) + self.assertIsInstance(self.place.latitude, float) + self.assertEqual(self.place.latitude, 0.0) def test_longitude(self): - """ """ - new = self.value() - self.assertEqual(type(new.latitude), float) + """Test that longitude exists and is a float""" + self.assertTrue(hasattr(self.place, "longitude")) + self.assertIsInstance(self.place.longitude, float) + self.assertEqual(self.place.longitude, 0.0) # Fixed the mistake def test_amenity_ids(self): - """ """ - new = self.value() - self.assertEqual(type(new.amenity_ids), list) + """Test that amenity_ids exists and is a list""" + self.assertTrue(hasattr(self.place, "amenity_ids")) + self.assertIsInstance(self.place.amenity_ids, list) + self.assertEqual(self.place.amenity_ids, []) From ab20561680ff6f9a0f7d8bb652c09d92979bcb34 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 14:33:23 +0300 Subject: [PATCH 062/101] refactor(tests/test_models/review.py): Added instantiation of review to setUp method and refactored the test methods --- tests/test_models/test_review.py | 36 +++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/tests/test_models/test_review.py b/tests/test_models/test_review.py index 23fbc61529e8..2c3c09c6565c 100755 --- a/tests/test_models/test_review.py +++ b/tests/test_models/test_review.py @@ -1,29 +1,37 @@ #!/usr/bin/python3 -""" """ -from tests.test_models.test_base_model import test_basemodel +# -*- coding: utf-8 -*- +"""Unit tests for Review""" +from tests.test_models.test_base_model import TestBaseModel from models.review import Review -class test_review(test_basemodel): - """ """ +class TestReview(TestBaseModel): + """Test cases for the Review class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "Review" self.value = Review + def setUp(self): + """Set up test environment""" + self.review = self.value() # Create a Review instance for testing + def test_place_id(self): - """ """ - new = self.value() - self.assertEqual(type(new.place_id), str) + """Test that place_id exists and is a string""" + self.assertTrue(hasattr(self.review, "place_id")) + self.assertIsInstance(self.review.place_id, str) + self.assertEqual(self.review.place_id, "") def test_user_id(self): - """ """ - new = self.value() - self.assertEqual(type(new.user_id), str) + """Test that user_id exists and is a string""" + self.assertTrue(hasattr(self.review, "user_id")) + self.assertIsInstance(self.review.user_id, str) + self.assertEqual(self.review.user_id, "") def test_text(self): - """ """ - new = self.value() - self.assertEqual(type(new.text), str) + """Test that text exists and is a string""" + self.assertTrue(hasattr(self.review, "text")) + self.assertIsInstance(self.review.text, str) + self.assertEqual(self.review.text, "") From a731604dc089edffa3b4863fdd96fc4d653c8cc5 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 14:42:36 +0300 Subject: [PATCH 063/101] test(tests/test_models/state.py): Added docstrings and additional assertions for test_name3 --- tests/test_models/test_state.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_models/test_state.py b/tests/test_models/test_state.py index 719e096d8633..894936971f56 100755 --- a/tests/test_models/test_state.py +++ b/tests/test_models/test_state.py @@ -1,19 +1,21 @@ #!/usr/bin/python3 -""" """ -from tests.test_models.test_base_model import test_basemodel +"""Unit tests for the State class""" +from tests.test_models.test_base_model import TestBaseModel from models.state import State -class test_state(test_basemodel): - """ """ +class test_state(TestBaseModel): + """Test cases for the State class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "State" self.value = State def test_name3(self): - """ """ + """Test that name exists and is a string""" new = self.value() - self.assertEqual(type(new.name), str) + self.assertTrue(hasattr(new, "name")) + self.assertIsInstance(new.name, str) + self.assertEqual(new.name, "") # Default value check From 8a5560166912c219a9ebc4666243dfa8dfbb55d1 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 14:52:50 +0300 Subject: [PATCH 064/101] test(tests/test_models/user.py): Added docstrings and additional assertions for each test method --- tests/test_models/test_user.py | 43 ++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/tests/test_models/test_user.py b/tests/test_models/test_user.py index 8660300f8bbc..3261133ee78b 100755 --- a/tests/test_models/test_user.py +++ b/tests/test_models/test_user.py @@ -1,34 +1,43 @@ #!/usr/bin/python3 -""" """ -from tests.test_models.test_base_model import test_basemodel +# -*- coding: utf-8 -*- +"""Unit tests for the User class""" +from tests.test_models.test_base_model import TestBaseModel from models.user import User -class test_User(test_basemodel): - """ """ +class test_User(TestBaseModel): + """Test cases for the User class""" def __init__(self, *args, **kwargs): - """ """ + """Initialize test class""" super().__init__(*args, **kwargs) self.name = "User" self.value = User + def setUp(self): + """Set up test environment""" + self.user = self.value() # Create a User instance for testing + def test_first_name(self): - """ """ - new = self.value() - self.assertEqual(type(new.first_name), str) + """Test that first_name exists and is a string""" + self.assertTrue(hasattr(self.user, "first_name")) + self.assertIsInstance(self.user.first_name, str) + self.assertEqual(self.user.first_name, "") # Default value check def test_last_name(self): - """ """ - new = self.value() - self.assertEqual(type(new.last_name), str) + """Test that last_name exists and is a string""" + self.assertTrue(hasattr(self.user, "last_name")) + self.assertIsInstance(self.user.last_name, str) + self.assertEqual(self.user.last_name, "") # Default value check def test_email(self): - """ """ - new = self.value() - self.assertEqual(type(new.email), str) + """Test that email exists and is a string""" + self.assertTrue(hasattr(self.user, "email")) + self.assertIsInstance(self.user.email, str) + self.assertEqual(self.user.email, "") # Default value check def test_password(self): - """ """ - new = self.value() - self.assertEqual(type(new.password), str) + """Test that password exists and is a string""" + self.assertTrue(hasattr(self.user, "password")) + self.assertIsInstance(self.user.password, str) + self.assertEqual(self.user.password, "") # Default value check From 52f2dbc1e76e7ca27dff33574cedd5313c3f6410 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 15:01:56 +0300 Subject: [PATCH 065/101] feat(console.py): Updated the do_create method to handle the = parameter syntax for instantiation with kwargs --- console.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/console.py b/console.py index 5b4c119101b5..7ba55bcff1b9 100755 --- a/console.py +++ b/console.py @@ -2,14 +2,16 @@ """ Console Module """ import cmd import sys +import re +import json from models.base_model import BaseModel -from models import storage from models.user import User from models.place import Place from models.state import State from models.city import City from models.amenity import Amenity from models.review import Review +from models import storage class HBNBCommand(cmd.Cmd): @@ -114,16 +116,65 @@ def emptyline(self): def do_create(self, args): """ Create an object of any class""" - if not args: + cls, _, params = args.partition(" ") + + if not cls: print("** class name missing **") return - elif args not in HBNBCommand.classes: + + if cls not in HBNBCommand.classes: print("** class doesn't exist **") return - new_instance = HBNBCommand.classes[args]() - storage.save() - print(new_instance.id) - storage.save() + + if params: + # substitute single quote for double to handle json parsing + params = re.sub(r'[\']', '"', params) + + # patterns for the attributes' values in the param syntax = + strings = r"""["'](?P[A-Za-z0-9_,!@#$%^&*\.-]+)["']""" + floats = r"(?P(?:[-]?[0-9]+(?=\.))(?:\.[0-9]*(?![\.0-9-]+))" + ints = r"(?P[-]?[0-9]+(?=\s))" + + # pattern for param syntax: = [=...] + attr_pattern = rf""" + \b(?P[A-Za-z_]+)(?=\=) # Attribute name + (?:\=) + (?P{strings}|(?P{ints}|{floats}))) # Attribute value + (?:\s*) + """ + + pattern = re.compile(attr_pattern, re.VERBOSE) + matches = pattern.findall(params) + + if matches: + attr_dict = {} + for match in matches: + key, value, str_val, num_val, _, _ = match + try: + val = json.loads(value) if str_val else json.loads(num_val) + + except (SyntaxError, json.JSONDecodeError) as e: + pass + + else: + if isinstance(val, str) and '_' in val: + translator = str.maketrans("_", " ") + attr_dict[key] = val.translate(translator) + else: + attr_dict[key] = val + + new_instance = HBNBCommand.classes[cls](**attr_dict) + new_instance.save() + print(new_instance.id) + else: + print("Invalid param syntax. ") + print("[Usage]: create = [=...]") + + else: + new_instance = HBNBCommand.classes[cls]() + new_instance.save() + print(new_instance.id) + def help_create(self): """ Help information for the create method """ From 9be3ffe3266439542d23b96d9ed890562f44d530 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 15:04:41 +0300 Subject: [PATCH 066/101] style(console.py): pycodestyle formatting --- console.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/console.py b/console.py index 7ba55bcff1b9..3e5d4e66145f 100755 --- a/console.py +++ b/console.py @@ -21,16 +21,16 @@ class HBNBCommand(cmd.Cmd): prompt = '(hbnb) ' if sys.__stdin__.isatty() else '' classes = { - 'BaseModel': BaseModel, 'User': User, 'Place': Place, - 'State': State, 'City': City, 'Amenity': Amenity, - 'Review': Review - } + 'BaseModel': BaseModel, 'User': User, 'Place': Place, + 'State': State, 'City': City, 'Amenity': Amenity, + 'Review': Review + } dot_cmds = ['all', 'count', 'show', 'destroy', 'update'] types = { - 'number_rooms': int, 'number_bathrooms': int, - 'max_guest': int, 'price_by_night': int, - 'latitude': float, 'longitude': float - } + 'number_rooms': int, 'number_bathrooms': int, + 'max_guest': int, 'price_by_night': int, + 'latitude': float, 'longitude': float + } def preloop(self): """Prints if isatty is false""" @@ -75,7 +75,7 @@ def precmd(self, line): pline = pline[2].strip() # pline is now str if pline: # check for *args or **kwargs - if pline[0] is '{' and pline[-1] is'}'\ + if pline[0] is '{' and pline[-1] is '}'\ and type(eval(pline)) is dict: _args = pline else: @@ -130,16 +130,18 @@ def do_create(self, args): # substitute single quote for double to handle json parsing params = re.sub(r'[\']', '"', params) - # patterns for the attributes' values in the param syntax = + # patterns for the attributes' values in the + # param syntax = strings = r"""["'](?P[A-Za-z0-9_,!@#$%^&*\.-]+)["']""" floats = r"(?P(?:[-]?[0-9]+(?=\.))(?:\.[0-9]*(?![\.0-9-]+))" ints = r"(?P[-]?[0-9]+(?=\s))" - # pattern for param syntax: = [=...] + # pattern for param syntax: + # = [=...] attr_pattern = rf""" \b(?P[A-Za-z_]+)(?=\=) # Attribute name (?:\=) - (?P{strings}|(?P{ints}|{floats}))) # Attribute value + (?P{strings}|(?P{ints}|{floats}))) # Attr value (?:\s*) """ @@ -151,7 +153,8 @@ def do_create(self, args): for match in matches: key, value, str_val, num_val, _, _ = match try: - val = json.loads(value) if str_val else json.loads(num_val) + val = json.loads( + value) if str_val else json.loads(num_val) except (SyntaxError, json.JSONDecodeError) as e: pass @@ -168,14 +171,14 @@ def do_create(self, args): print(new_instance.id) else: print("Invalid param syntax. ") - print("[Usage]: create = [=...]") + print("[Usage]: create =" + + " [=...]") else: new_instance = HBNBCommand.classes[cls]() new_instance.save() print(new_instance.id) - def help_create(self): """ Help information for the create method """ print("Creates a class of any type") @@ -237,7 +240,7 @@ def do_destroy(self, args): key = c_name + "." + c_id try: - del(storage.all()[key]) + del (storage.all()[key]) storage.save() except KeyError: print("** no instance found **") @@ -370,5 +373,6 @@ def help_update(self): print("Updates an object with new information") print("Usage: update \n") + if __name__ == "__main__": HBNBCommand().cmdloop() From 8efeabd761daf018b276b3a04c684654baa90c28 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 15:17:25 +0300 Subject: [PATCH 067/101] refactor(console.py): Refactored do_show to call storage.all --- console.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/console.py b/console.py index 3e5d4e66145f..718fcb76c11c 100755 --- a/console.py +++ b/console.py @@ -1,5 +1,5 @@ #!/usr/bin/python3 -""" Console Module """ +"""Console Module""" import cmd import sys import re @@ -15,8 +15,7 @@ class HBNBCommand(cmd.Cmd): - """ Contains the functionality for the HBNB console""" - + """Contains the functionality for the HBNB console""" # determines prompt for interactive/non-interactive modes prompt = '(hbnb) ' if sys.__stdin__.isatty() else '' @@ -25,7 +24,9 @@ class HBNBCommand(cmd.Cmd): 'State': State, 'City': City, 'Amenity': Amenity, 'Review': Review } + dot_cmds = ['all', 'count', 'show', 'destroy', 'update'] + types = { 'number_rooms': int, 'number_bathrooms': int, 'max_guest': int, 'price_by_night': int, @@ -95,27 +96,27 @@ def postcmd(self, stop, line): return stop def do_quit(self, command): - """ Method to exit the HBNB console""" + """Method to exit the HBNB console""" return True def help_quit(self): - """ Prints the help documentation for quit """ + """Prints the help documentation for quit """ print("Exits the program with formatting\n") def do_EOF(self, arg): - """ Handles EOF to exit program """ + """Handles EOF to exit program""" return True def help_EOF(self): - """ Prints the help documentation for EOF """ + """Prints the help documentation for EOF""" print("Exits the program without formatting\n") def emptyline(self): - """ Overrides the emptyline method of CMD """ + """Overrides the emptyline method of CMD""" pass def do_create(self, args): - """ Create an object of any class""" + """Create an object of any class""" cls, _, params = args.partition(" ") if not cls: @@ -144,7 +145,6 @@ def do_create(self, args): (?P{strings}|(?P{ints}|{floats}))) # Attr value (?:\s*) """ - pattern = re.compile(attr_pattern, re.VERBOSE) matches = pattern.findall(params) @@ -180,12 +180,12 @@ def do_create(self, args): print(new_instance.id) def help_create(self): - """ Help information for the create method """ + """ Help information for the create method""" print("Creates a class of any type") print("[Usage]: create \n") def do_show(self, args): - """ Method to show an individual object """ + """ Method to show an individual object""" new = args.partition(" ") c_name = new[0] c_id = new[2] @@ -208,17 +208,17 @@ def do_show(self, args): key = c_name + "." + c_id try: - print(storage._FileStorage__objects[key]) + print(storage.all()[key]) except KeyError: print("** no instance found **") def help_show(self): - """ Help information for the show command """ + """ Help information for the show command""" print("Shows an individual instance of a class") print("[Usage]: show \n") def do_destroy(self, args): - """ Destroys a specified object """ + """ Destroys a specified object""" new = args.partition(" ") c_name = new[0] c_id = new[2] @@ -246,7 +246,7 @@ def do_destroy(self, args): print("** no instance found **") def help_destroy(self): - """ Help information for the destroy command """ + """ Help information for the destroy command""" print("Destroys an individual instance of a class") print("[Usage]: destroy \n") @@ -269,7 +269,7 @@ def do_all(self, args): print(print_list) def help_all(self): - """ Help information for the all command """ + """ Help information for the all command""" print("Shows all objects, or all of a class") print("[Usage]: all \n") @@ -286,7 +286,7 @@ def help_count(self): print("Usage: count ") def do_update(self, args): - """ Updates a certain object with new info """ + """ Updates a certain object with new info""" c_name = c_id = att_name = att_val = kwargs = '' # isolate cls from id/args, ex: (, delim, ) @@ -369,7 +369,7 @@ def do_update(self, args): new_dict.save() # save updates to file def help_update(self): - """ Help information for the update class """ + """ Help information for the update class""" print("Updates an object with new information") print("Usage: update \n") From 1083dd15342653537467d4105dd6f08ec432f812 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 15:21:39 +0300 Subject: [PATCH 068/101] refactor(console.py): refactored do_all to call storage.all --- console.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/console.py b/console.py index 718fcb76c11c..6fea0e9cec67 100755 --- a/console.py +++ b/console.py @@ -246,30 +246,30 @@ def do_destroy(self, args): print("** no instance found **") def help_destroy(self): - """ Help information for the destroy command""" + """Help information for the destroy command.""" print("Destroys an individual instance of a class") print("[Usage]: destroy \n") def do_all(self, args): - """ Shows all objects, or all objects of a class""" + """Show all objects, or all objects of a class.""" print_list = [] if args: - args = args.split(' ')[0] # remove possible trailing args - if args not in HBNBCommand.classes: + cls = args.split(' ')[0] # remove possible trailing args + if cls not in HBNBCommand.classes: print("** class doesn't exist **") return - for k, v in storage._FileStorage__objects.items(): - if k.split('.')[0] == args: - print_list.append(str(v)) + for key, value in storage.all().items(): + if key.split('.')[0] == cls: + print_list.append(str(value)) else: - for k, v in storage._FileStorage__objects.items(): - print_list.append(str(v)) + for key, value in storage.all().items(): + print_list.append(str(value)) print(print_list) def help_all(self): - """ Help information for the all command""" + """Help information for the all command""" print("Shows all objects, or all of a class") print("[Usage]: all \n") From a5737f442be825cb2ce0ee932a20606940a8c547 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 15:25:29 +0300 Subject: [PATCH 069/101] refactor(console.py): refactored do_count to call storage.all --- console.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/console.py b/console.py index 6fea0e9cec67..1eb7c582297b 100755 --- a/console.py +++ b/console.py @@ -218,7 +218,7 @@ def help_show(self): print("[Usage]: show \n") def do_destroy(self, args): - """ Destroys a specified object""" + """Destroy a specified object.""" new = args.partition(" ") c_name = new[0] c_id = new[2] @@ -269,24 +269,27 @@ def do_all(self, args): print(print_list) def help_all(self): - """Help information for the all command""" + """Help information for the all command.""" print("Shows all objects, or all of a class") print("[Usage]: all \n") def do_count(self, args): - """Count current number of class instances""" + """Count current number of class instances.""" + storage.reload() # added Explicitly reload storage + count = 0 - for k, v in storage._FileStorage__objects.items(): + for k, v in storage.all().items(): if args == k.split('.')[0]: count += 1 + print(count) def help_count(self): - """ """ + """Help information for the count command.""" print("Usage: count ") def do_update(self, args): - """ Updates a certain object with new info""" + """Update a certain object with new info.""" c_name = c_id = att_name = att_val = kwargs = '' # isolate cls from id/args, ex: (, delim, ) From b1d03c2cb0b3963acd0a8721020768343e6fcd8d Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 15:33:16 +0300 Subject: [PATCH 070/101] refactor(console.py): refactored precmd to use comparison operator for equality test --- console.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/console.py b/console.py index 1eb7c582297b..abee8098a80d 100755 --- a/console.py +++ b/console.py @@ -76,7 +76,7 @@ def precmd(self, line): pline = pline[2].strip() # pline is now str if pline: # check for *args or **kwargs - if pline[0] is '{' and pline[-1] is '}'\ + if pline[0] == '{' and pline[-1] == '}'\ and type(eval(pline)) is dict: _args = pline else: @@ -372,7 +372,7 @@ def do_update(self, args): new_dict.save() # save updates to file def help_update(self): - """ Help information for the update class""" + """Help information for the update class.""" print("Updates an object with new information") print("Usage: update \n") From 534c45c5d59950c34836598bf640f1e7e18d32ea Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 15:49:13 +0300 Subject: [PATCH 071/101] test(tests/test_models/test_engine/test_file_storage.py): Added test for the delete method and test classes for each model --- .../test_engine/test_file_storage.py | 160 ++++++++++++++---- 1 file changed, 123 insertions(+), 37 deletions(-) diff --git a/tests/test_models/test_engine/test_file_storage.py b/tests/test_models/test_engine/test_file_storage.py index e1de7198b697..d747892821c2 100755 --- a/tests/test_models/test_engine/test_file_storage.py +++ b/tests/test_models/test_engine/test_file_storage.py @@ -1,26 +1,40 @@ #!/usr/bin/python3 """ Module for testing file storage""" +import os import unittest from models.base_model import BaseModel +from models.user import User +from models.state import State +from models.city import City +from models.amenity import Amenity +from models.place import Place +from models.review import Review from models import storage -import os +from models.engine.file_storage import FileStorage -class test_fileStorage(unittest.TestCase): +@unittest.skipIf(os.getenv("HBNB_TYPE_STORAGE") == "db", + "Skipping: not using DBStorage") +class BaseTestFileStorage(unittest.TestCase): """ Class to test the file storage method """ + model = None + __file = "file.json" + def setUp(self): """ Set up test environment """ - del_list = [] - for key in storage._FileStorage__objects.keys(): - del_list.append(key) - for key in del_list: - del storage._FileStorage__objects[key] + if self.model is None: + self.skipTest("Skipping BaseTestFileStorage since it has no model") + + if os.path.exists(self.__file): + os.remove(self.__file) + + storage._FileStorage__objects.clear() def tearDown(self): """ Remove storage file at end of tests """ try: - os.remove('file.json') + os.remove(self.__file) except: pass @@ -30,48 +44,53 @@ def test_obj_list_empty(self): def test_new(self): """ New object is correctly added to __objects """ - new = BaseModel() - for obj in storage.all().values(): - temp = obj - self.assertTrue(temp is obj) + new = self.model() + storage.new(new) # added + + self.assertIn(new, storage.all().values()) def test_all(self): """ __objects is properly returned """ - new = BaseModel() + new = self.model() + storage.new(new) # added + storage.save() # added temp = storage.all() self.assertIsInstance(temp, dict) + self.assertIn(new, storage.all().values()) - def test_base_model_instantiation(self): - """ File is not created on BaseModel save """ - new = BaseModel() - self.assertFalse(os.path.exists('file.json')) + def test_model_instantiation(self): + """ Json file is not created on a model instantiation """ + new = self.model() + self.assertFalse(os.path.exists(self.__file)) def test_empty(self): """ Data is saved to file """ - new = BaseModel() + new = self.model() thing = new.to_dict() new.save() - new2 = BaseModel(**thing) - self.assertNotEqual(os.path.getsize('file.json'), 0) + new2 = self.model(**thing) + self.assertNotEqual(os.path.getsize(self.__file), 0) def test_save(self): """ FileStorage save method """ - new = BaseModel() + new = self.model() + storage.new(new) # added storage.save() - self.assertTrue(os.path.exists('file.json')) + self.assertTrue(os.path.exists(self.__file)) def test_reload(self): """ Storage file is successfully loaded to __objects """ - new = BaseModel() + new = self.model() + storage.new(new) # added storage.save() storage.reload() - for obj in storage.all().values(): - loaded = obj - self.assertEqual(new.to_dict()['id'], loaded.to_dict()['id']) + + self.assertEqual(new.to_dict()['id'], + list(storage.all().values())[0].id) def test_reload_empty(self): """ Load from an empty file """ - with open('file.json', 'w') as f: + with open(self.__file, 'w') as f: pass with self.assertRaises(ValueError): storage.reload() @@ -82,9 +101,9 @@ def test_reload_from_nonexistent(self): def test_base_model_save(self): """ BaseModel save method calls storage save """ - new = BaseModel() + new = self.model() new.save() - self.assertTrue(os.path.exists('file.json')) + self.assertTrue(os.path.exists(self.__file)) def test_type_path(self): """ Confirm __file_path is string """ @@ -92,18 +111,85 @@ def test_type_path(self): def test_type_objects(self): """ Confirm __objects is a dict """ - self.assertEqual(type(storage.all()), dict) + self.assertTrue(type(storage.all()) is dict) def test_key_format(self): """ Key is properly formatted """ - new = BaseModel() + new = self.model() + storage.new(new) # added _id = new.to_dict()['id'] - for key in storage.all().keys(): - temp = key - self.assertEqual(temp, 'BaseModel' + '.' + _id) + + self.assertIn(f'{self.model.__name__}' + '.' + _id, + storage.all().keys()) def test_storage_var_created(self): """ FileStorage object storage created """ - from models.engine.file_storage import FileStorage - print(type(storage)) - self.assertEqual(type(storage), FileStorage) + self.assertIsInstance(storage, FileStorage) + + def test_delete(self): + """ Object is successfully deleted from __objects """ + new = self.model() + storage.new(new) + storage.save() + self.assertIn(new, storage.all().values()) + storage.delete(new) + self.assertNotIn(new, storage.all().values()) + + def test_delete_nonexistent(self): + """ Deleting a nonexistent object should not raise an error """ + new = self.model() + storage.new(new) + storage.save() + self.assertIn(new, storage.all().values()) + storage.delete(new) # Should not raise an error + self.assertNotIn(new, storage.all().values()) + + def test_all_with_class(self): + """ all() method with class argument returns only matching objects """ + new1 = self.model() + new2 = self.model() + storage.new(new1) + storage.new(new2) + storage.save() + self.assertIn(new1, storage.all(self.model).values()) + self.assertIn(new2, storage.all(self.model).values()) + self.assertTrue(all(self.assertIn(obj, + storage.all().values())) + for obj in (new1, new2)) + + def test_get_number_of_records(self): + """ Get the number of current records in storage """ + new1 = self.model() + new2 = self.model() + storage.new(new1) + storage.new(new2) + storage.save() + self.assertEqual(len(storage.all()), 2) + + +class TestBaseModelStorage(BaseTestFileStorage): + model = BaseModel + + +class TestCityStorage(BaseTestFileStorage): + model = City + + +class TestAmenityStorage(BaseTestFileStorage): + model = Amenity + + +class TestPlaceStorage(BaseTestFileStorage): + model = Place + + +class TestReviewStorage(BaseTestFileStorage): + model = Review + + +class TestUserStorage(BaseTestFileStorage): + model = User + + +class TestStateStorage(BaseTestFileStorage): + model = State From e32e93611535737d13d849930acdbc03f7393d49 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 15:50:59 +0300 Subject: [PATCH 072/101] style(tests/test_models/test_engine/test_file_storage.py): pycodestyle formatting --- tests/test_models/test_engine/test_file_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_models/test_engine/test_file_storage.py b/tests/test_models/test_engine/test_file_storage.py index d747892821c2..354ef61ffe12 100755 --- a/tests/test_models/test_engine/test_file_storage.py +++ b/tests/test_models/test_engine/test_file_storage.py @@ -35,7 +35,7 @@ def tearDown(self): """ Remove storage file at end of tests """ try: os.remove(self.__file) - except: + except FileNotFoundError: pass def test_obj_list_empty(self): From e92953c4a295dbe04fdf2a24292d73561935d165 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 15:54:02 +0300 Subject: [PATCH 073/101] style(tests/test_console.py): pycodestyle formatting --- tests/test_console.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_console.py b/tests/test_console.py index 4f72847b4718..84b21ca61521 100755 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -313,7 +313,6 @@ class TestDotCommands(TestFileStorageConsole): def setUp(self): """Set up the test environment by creating a FileStorage instance and clearing the file.""" - print("setUp: - > ", self._classes) for cls in self._classes: with patch('sys.stdout', new=StringIO()) as output: HBNBCommand().onecmd(f"create {cls}") From 0d3e2efdcc3173010d5f0501e2a3ace12447f785 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 15:55:20 +0300 Subject: [PATCH 074/101] style(console): pycodestyle formatting --- console.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/console.py b/console.py index abee8098a80d..be2fbbe7dbd3 100755 --- a/console.py +++ b/console.py @@ -1,5 +1,5 @@ #!/usr/bin/python3 -"""Console Module""" +"""Console Module.""" import cmd import sys import re @@ -16,6 +16,7 @@ class HBNBCommand(cmd.Cmd): """Contains the functionality for the HBNB console""" + # determines prompt for interactive/non-interactive modes prompt = '(hbnb) ' if sys.__stdin__.isatty() else '' @@ -34,7 +35,7 @@ class HBNBCommand(cmd.Cmd): } def preloop(self): - """Prints if isatty is false""" + """Print if isatty is false.""" if not sys.__stdin__.isatty(): print('(hbnb)') @@ -86,37 +87,38 @@ def precmd(self, line): except Exception as mess: pass + finally: return line def postcmd(self, stop, line): - """Prints if isatty is false""" + """Prints if isatty is false.""" if not sys.__stdin__.isatty(): print('(hbnb) ', end='') return stop def do_quit(self, command): - """Method to exit the HBNB console""" + """Method to exit the HBNB console.""" return True def help_quit(self): - """Prints the help documentation for quit """ + """Prints the help documentation for quit.""" print("Exits the program with formatting\n") def do_EOF(self, arg): - """Handles EOF to exit program""" + """Handles EOF to exit program.""" return True def help_EOF(self): - """Prints the help documentation for EOF""" + """Prints the help documentation for EOF.""" print("Exits the program without formatting\n") def emptyline(self): - """Overrides the emptyline method of CMD""" + """Overrides the emptyline method of CMD.""" pass def do_create(self, args): - """Create an object of any class""" + """Create an object of any class.""" cls, _, params = args.partition(" ") if not cls: @@ -180,12 +182,12 @@ def do_create(self, args): print(new_instance.id) def help_create(self): - """ Help information for the create method""" + """Help information for the create method.""" print("Creates a class of any type") print("[Usage]: create \n") def do_show(self, args): - """ Method to show an individual object""" + """Method to show an individual object.""" new = args.partition(" ") c_name = new[0] c_id = new[2] @@ -213,7 +215,7 @@ def do_show(self, args): print("** no instance found **") def help_show(self): - """ Help information for the show command""" + """Help information for the show command.""" print("Shows an individual instance of a class") print("[Usage]: show \n") From efeb973a43ac69267b3ed331fbcd1ea6b29c442a Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 16:01:27 +0300 Subject: [PATCH 075/101] refactor(console.py): refactor to use comparison operators for equality tests --- console.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/console.py b/console.py index be2fbbe7dbd3..8cbd6eac23a3 100755 --- a/console.py +++ b/console.py @@ -330,7 +330,7 @@ def do_update(self, args): args.append(v) else: # isolate args args = args[2] - if args and args[0] is '\"': # check for quoted arg + if args and args[0] == '\"': # check for quoted arg second_quote = args.find('\"', 1) att_name = args[1:second_quote] args = args[second_quote + 1:] @@ -338,10 +338,10 @@ def do_update(self, args): args = args.partition(' ') # if att_name was not quoted arg - if not att_name and args[0] is not ' ': + if not att_name and args[0] != ' ': att_name = args[0] # check for quoted val arg - if args[2] and args[2][0] is '\"': + if args[2] and args[2][0] == '\"': att_val = args[2][1:args[2].find('\"', 1)] # if att_val was not quoted arg From 51355dcd5fc2f1f784d9dfd37a28715cecf43813 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 16:11:40 +0300 Subject: [PATCH 076/101] CHANGELOG.md added --- CHANGELOG.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e86c552656b7..807029767e6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,27 @@ ## [Unreleased] -### Chores -- **setup_mysql_dev.sql:** Added MySQL setup file for development -- **setup_mysql_test.sql:** Added MySQL setup file for testing +### Code Refactoring +- **console.py:** refactor to use comparison operators for equality tests +- **console.py:** refactored precmd to use comparison operator for equality test +- **console.py:** refactored do_count to call storage.all +- **console.py:** refactored do_all to call storage.all +- **console.py:** Refactored do_show to call storage.all + +### Features +- **console.py:** Updated the do_create method to handle the = parameter syntax for instantiation with kwargs + +### Styles +- **console:** pycodestyle formatting +- **console.py:** pycodestyle formatting ## [v2.1.0] - 2025-02-12 +### Chores +- **setup_mysql_dev.sql:** Added MySQL setup file for development ([f34c4e0](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f34c4e0f3c21d8c0d5996b87cae25969fe1323ea)) +- **setup_mysql_test.sql:** Added MySQL setup file for testing ([59eeb6e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/59eeb6eed7268f39d8ed6d3f66d8fcaab73e3dbd)) + ### Code Refactoring - **console.py:** Refactored storage import from models ([8fcd9e7](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8fcd9e75692ff88124b7e5fa148e98db35337d0f)) - **console.py:** Refactored exit() and print() with return True in do_EOF method ([d534c1e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d534c1e7a511d17424e8fcbdbe4c5ea7985d30a6)) From c30de2ae3157b0ec40326c863091bf53030c92d3 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 16:38:22 +0300 Subject: [PATCH 077/101] CHANGELOG.md updated --- CHANGELOG.md | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 807029767e6e..87f787471e6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,6 @@ -# CHANGELOG - ## [Unreleased] -### Code Refactoring -- **console.py:** refactor to use comparison operators for equality tests -- **console.py:** refactored precmd to use comparison operator for equality test -- **console.py:** refactored do_count to call storage.all -- **console.py:** refactored do_all to call storage.all -- **console.py:** Refactored do_show to call storage.all - -### Features -- **console.py:** Updated the do_create method to handle the = parameter syntax for instantiation with kwargs - -### Styles -- **console:** pycodestyle formatting -- **console.py:** pycodestyle formatting - ## [v2.1.0] - 2025-02-12 @@ -25,6 +9,11 @@ - **setup_mysql_test.sql:** Added MySQL setup file for testing ([59eeb6e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/59eeb6eed7268f39d8ed6d3f66d8fcaab73e3dbd)) ### Code Refactoring +- **console.py:** refactor to use comparison operators for equality tests ([efeb973](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/efeb973a43ac69267b3ed331fbcd1ea6b29c442a)) +- **console.py:** refactored precmd to use comparison operator for equality test ([b1d03c2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/b1d03c2cb0b3963acd0a8721020768343e6fcd8d)) +- **console.py:** refactored do_count to call storage.all ([a5737f4](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a5737f442be825cb2ce0ee932a20606940a8c547)) +- **console.py:** refactored do_all to call storage.all ([1083dd1](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/1083dd15342653537467d4105dd6f08ec432f812)) +- **console.py:** Refactored do_show to call storage.all ([8efeabd](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8efeabd761daf018b276b3a04c684654baa90c28)) - **console.py:** Refactored storage import from models ([8fcd9e7](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8fcd9e75692ff88124b7e5fa148e98db35337d0f)) - **console.py:** Refactored exit() and print() with return True in do_EOF method ([d534c1e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d534c1e7a511d17424e8fcbdbe4c5ea7985d30a6)) - **console.py:** Refactored exit() with return True in do_quit method ([ad891f6](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ad891f6bae73541cf1153c37646596dd0bcb6f94)) @@ -36,6 +25,13 @@ - **CHANGELOG.md:** Updated to show commit links ([169356d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/169356dbc9b8542c9b1591745f377f42afaaf176)) - **CHANGELOG.md:** Added change log file ([d976a1d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d976a1d761adba4d73b8c8e364e8fd3af6d65df6)) +### Features +- **console.py:** Updated the do_create method to handle the = parameter syntax for instantiation with kwargs ([52f2dbc](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/52f2dbc1e76e7ca27dff33574cedd5313c3f6410)) + +### Styles +- **console:** pycodestyle formatting ([0d3e2ef](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/0d3e2efdcc3173010d5f0501e2a3ace12447f785)) +- **console.py:** pycodestyle formatting ([9be3ffe](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/9be3ffe3266439542d23b96d9ed890562f44d530)) + ## v2.0.0 - 2025-02-11 From b1d3c0fc82d789eb40865cdcc1ccc4be7978240e Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 16:59:14 +0300 Subject: [PATCH 078/101] updated CHANGELOG --- CHANGELOG.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87f787471e6a..762fea0db496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,24 @@ - **console.py:** Refactored storage import from models ([8fcd9e7](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8fcd9e75692ff88124b7e5fa148e98db35337d0f)) - **console.py:** Refactored exit() and print() with return True in do_EOF method ([d534c1e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d534c1e7a511d17424e8fcbdbe4c5ea7985d30a6)) - **console.py:** Refactored exit() with return True in do_quit method ([ad891f6](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ad891f6bae73541cf1153c37646596dd0bcb6f94)) +- **console.py:** refactor to use comparison operators for equality tests ([efeb973](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/efeb973)) +- **console.py:** refactored precmd to use comparison operator for equality test ([b1d03c2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/b1d03c2)) +- **console.py:** refactored do_count to call storage.all ([a5737f4](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a5737f4)) +- **console.py:** refactored do_all to call storage.all ([1083dd1](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/1083dd1)) +- **console.py:** Refactored do_show to call storage.all ([8efeabd](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8efeabd)) +- **tests/test_models/review.py:** Added instantiation of review to setUp method and refactored the test methods ([ab20561](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ab20561)) +- **tests/test_models/place.py:** Added instantiation of place to setUp method and refactored the test methods ([f843c72](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f843c72)) +- **models/base_model.py:** Refactored to_dict method to iterate over __dict__ attribute and ignore the _sa_instance_state attribute ([ce431f5](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ce431f5)) +- **models/base_model.py:** Refactored save method to call storage.new method ([71c19ff](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/71c19ff)) +- **models/base_model.py:** Refactored __str__ to use f-strings for the return string and to removed the _sa_instance_state attribute from the string representation ([a38b99e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a38b99e)) +- **models/base_model.py:** Refactored __init__ to iterate over kwargs and update attributes using setattr function. Removed storage import from __init__ and calling of the storage.new method ([d788011](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d788011)) +- **models/engine/file_storage.py:** Refactored reload method to use self to access FileStorage__file_path and to load data to FileStorage__objects ([62bc9ce](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/62bc9ce)) +- **models/engine/file_storage.py:** Refactored save method to use dictionary comprehension ([9303c93](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/9303c93)) +- **models/engine/file_storage.py:** Refactored save method to use f-strings for key and self to update FileStorage__objects ([6578f5a](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/6578f5a)) +- **console.py:** Refactored storage import from models ([8fcd9e7](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8fcd9e7)) +- **console.py:** Refactored exit() and print() with return True in do_EOF method ([d534c1e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d534c1e)) +- **console.py:** Refactored exit() with return True in do_quit method ([ad891f6](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ad891f6)) +- **tests/test_models/amenity.py:** Renamed class to Pascal naming style ([b4b5a96](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/b4b5a96)) ### Documentation - **AUTHORS:** Added 'Albert Mwanza to authors' ([eb1305e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/eb1305e1b5bbad2ceae12d7b92fd9f3f1dbc1345)) @@ -24,14 +42,71 @@ - **CHANGELOG.md:** Updated to show commit links ([f2dd2a2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f2dd2a296a9efdba2e8759482e1ddc2fd1a9c9b1)) - **CHANGELOG.md:** Updated to show commit links ([169356d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/169356dbc9b8542c9b1591745f377f42afaaf176)) - **CHANGELOG.md:** Added change log file ([d976a1d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d976a1d761adba4d73b8c8e364e8fd3af6d65df6)) +- **tests/test_models/city.py:** Added docstrings ([de81e99](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/de81e99)) +- **tests/test_models/amenity.py:** Added docstrings ([d70a4eb](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d70a4eb)) +- **tests/test_models/base_model.py:** Added module and method docstrings ([1ed07ba](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/1ed07ba)) +- **CHANGELOG.md:** Added change log file ([d976a1d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/d976a1d)) +- **/CHANGELOG.md:** Added commit links ([2dbd158](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/2dbd158)) +- **/CHANGELOG.md:** Updated log to include commit links ([c97d457](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/c97d457)) +- **/CHANGELOG.md:** Added title ([067afbe](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/067afbe)) +- **/CHANGELOG.md:** Added title ([7081a34](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/7081a34)) +- **/CHANGELOG.md:** Updated changelog ([29d9569](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/29d9569)) +- **CHANGELOG.md:** Updated changelog ([ed51879](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/ed51879)) +- CHANGELOG.md added ([7d81b6f](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/7d81b6f)) +- Add Albert Mwanza to AUTHORS file ([e574683](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/e574683)) + ### Features - **console.py:** Updated the do_create method to handle the = parameter syntax for instantiation with kwargs ([52f2dbc](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/52f2dbc1e76e7ca27dff33574cedd5313c3f6410)) +- **models/user.py:** Updated class with attributes for SLQAlchemy table mapping ([10f26ef](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/10f26ef)) +- **models/state.py:** Update class with attributes for SQLAlchemy table mapping ([6725fe2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/6725fe2)) +- **model/review.py:** Updated class with attributes for SQLAlchemy table mapping ([166e957](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/166e957)) +- **models/place.py:** Updated class with class attributes for ORM mapping. Added association table for many-to-many relationship mapping between places and amenities tables. Added property and setter methods for use with FileStorage. ([22baca0](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/22baca0)) +- **models/city.py:** Added class attributes for SQLAlchemy table mapping and relationship mapping ([a8c7707](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a8c7707)) +- **models/amenity.py:** Updated class Amenity to Inherit from BaseModel and Base. Added class attributes for SQLAlchemy table mapping and Many to Many relationship mapping with Places ([764f169](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/764f169)) +- **models/base_model.py:** Added delete method to call storage.delete method to delete object from storage ([984e473](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/984e473)) +- **models/basem_model.py:** Added class attributes for SQLAlchemy table mapping ([a2cbcac](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a2cbcac)) +- **models/engine/db_storage.py:** Added DBStorage module for managing database storage in MySQL ([50988b8](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/50988b8)) +- **models/__init__.py:** Updated to instantiate storage based on the environment variable HBNB_TYPE_STORAGE ([0244162](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/0244162)) +- **models/engine/file_storage.py:** Updated all method to allow filtering based on class ([c773e45](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/c773e45)) +- **models/engine/file_storage.py:** Added a new public instance method to delete objects from storage ([a295eb8](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a295eb8)) + ### Styles - **console:** pycodestyle formatting ([0d3e2ef](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/0d3e2efdcc3173010d5f0501e2a3ace12447f785)) - **console.py:** pycodestyle formatting ([9be3ffe](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/9be3ffe3266439542d23b96d9ed890562f44d530)) +- **console:** pycodestyle formatting ([0d3e2ef](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/0d3e2ef)) +- **tests/test_console.py:** pycodestyle formatting ([e92953c](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/e92953c)) +- **tests/test_models/test_engine/test_file_storage.py:** pycodestyle formatting ([e32e936](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/e32e936)) +- **console.py:** pycodestyle formatting ([9be3ffe](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/9be3ffe)) +- **tests/test_models/city.py:** Renamed imported class test_base_model to pascal case TestBaseModel ([fa5dc8e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/fa5dc8e)) +- **tests/test_models/amenity.py:** Renamed imported class test_base_model to pascal case TestBaseModel ([62ff0d2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/62ff0d2)) +- **tests/test_models/base_model.py:** renamed class name to conform to PascalCase ([83d8a3d](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/83d8a3d)) +- **tests/test_models/test_engine/test_db_storage.py:** pycodestyle formatting ([c0ce84b](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/c0ce84b)) +- **models/base_model.py:** pycodestyle formatting ([96d660f](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/96d660f)) +- **models/engine/file_storage.py:** Pycodestyle Formatting ([fcedbb6](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/fcedbb6)) +### Tests +- **tests/test_models/test_engine/test_file_storage.py:** Added test for the delete method and test classes for each model ([534c45c](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/534c45c)) +- **tests/test_models/user.py:** Added docstrings and additional assertions for each test method ([8a55601](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/8a55601)) +- **tests/test_models/state.py:** Added docstrings and additional assertions for test_name3 ([a731604](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a731604)) +- **tests/test_models/test_engine/test_db_storage.py:** Add tests for DBStorage ([3dd588b](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/3dd588b)) +- **tests/test_console.py:** Add console tests for file and database storage ([9a8fc14](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/9a8fc14)) + +### Chores +- **models/state.py:** Added executable rights ([f75d27f](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f75d27f)) +- **models/place.py:** Added executable rights ([7eae5aa](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/7eae5aa)) +- **models/city.py:** Added executable rights ([4d8948c](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/4d8948c)) +- **models/amenity.py:** Added executable rights ([2d497fb](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/2d497fb)) +- **models/engine/file_storage.py:** Added encoding declaration ([37d9454](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/37d9454)) +- **models/__init__.py:** Added executable rights ([38af25e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/38af25e)) +- **models/__init_.py:** Added encoding declaration ([b767157](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/b767157)) +- **models/engine/__init__.py:** Added executable rights ([c1467b9](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/c1467b9)) +- **models/engine/file_storage.py:** Added executable rights ([a313ed6](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/a313ed6)) +- **setup_mysql_test.sql:** Added MySQL setup file for testing ([59eeb6e](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/59eeb6e)) +- **setup_mysql_dev.sql:** Added MySQL setup file for development ([f34c4e0](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/f34c4e0)) +- **setup_mysql_dev.sql:** Added MySQL setup file for development ([628f4a2](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/628f4a2)) +- **tests/__init__:** Added executable rights ([0775e3f](https://github.com/mwanzaalbert/AirBnB_clone_v2/commit/0775e3f)) ## v2.0.0 - 2025-02-11 From 55261db3a3a27c7eb5b3e4266a6e86dab772cce5 Mon Sep 17 00:00:00 2001 From: mwanzaalbert <71031723+mwanzaalbert@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:02:08 +0300 Subject: [PATCH 079/101] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 762fea0db496..b9bf900d0749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ - -## [Unreleased] + +## [Final version] From 6c8416732b4816e57b4666b32f00fef7b25929d2 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Wed, 12 Feb 2025 17:03:42 +0300 Subject: [PATCH 080/101] README.md updated --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3ce462902d67..f27d6bf173ba 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,17 @@ This repository contains the initial stage of a student project to build a clone | Tasks | Files | Description | | ----- | ----- | ------ | -| 0: Authors/README File | [AUTHORS](https://github.com/justinmajetich/AirBnB_clone/blob/dev/AUTHORS) | Project authors | +| 0: Authors/README File | [AUTHORS](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/master/AUTHORS) | Project authors | | 1: Pep8 | N/A | All code is pep8 compliant| -| 2: Unit Testing | [/tests](https://github.com/justinmajetich/AirBnB_clone/tree/dev/tests) | All class-defining modules are unittested | -| 3. Make BaseModel | [/models/base_model.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/base_model.py) | Defines a parent class to be inherited by all model classes| -| 4. Update BaseModel w/ kwargs | [/models/base_model.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/base_model.py) | Add functionality to recreate an instance of a class from a dictionary representation| -| 5. Create FileStorage class | [/models/engine/file_storage.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/engine/file_storage.py) [/models/_ _init_ _.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/__init__.py) [/models/base_model.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/base_model.py) | Defines a class to manage persistent file storage system| -| 6. Console 0.0.1 | [console.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/console.py) | Add basic functionality to console program, allowing it to quit, handle empty lines and ^D | -| 7. Console 0.1 | [console.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/console.py) | Update the console with methods allowing the user to create, destroy, show, and update stored data | -| 8. Create User class | [console.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/console.py) [/models/engine/file_storage.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/engine/file_storage.py) [/models/user.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/user.py) | Dynamically implements a user class | -| 9. More Classes | [/models/user.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/user.py) [/models/place.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/place.py) [/models/city.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/city.py) [/models/amenity.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/amenity.py) [/models/state.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/state.py) [/models/review.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/review.py) | Dynamically implements more classes | -| 10. Console 1.0 | [console.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/console.py) [/models/engine/file_storage.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/engine/file_storage.py) | Update the console and file storage system to work dynamically with all classes update file storage | +| 2: Unit Testing | [/tests](https://github.com/mwanzaalbert/AirBnB_clone_v2/tree/dev/tests) | All class-defining modules are unittested | +| 3. Make BaseModel | [/models/base_model.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/base_model.py) | Defines a parent class to be inherited by all model classes| +| 4. Update BaseModel w/ kwargs | [/models/base_model.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/base_model.py) | Add functionality to recreate an instance of a class from a dictionary representation| +| 5. Create FileStorage class | [/models/engine/file_storage.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/engine/file_storage.py) [/models/_ _init_ _.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/__init__.py) [/models/base_model.py]https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/base_model.py) | Defines a class to manage persistent file storage system| +| 6. Console 0.0.1 | [console.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/console.py) | Add basic functionality to console program, allowing it to quit, handle empty lines and ^D | +| 7. Console 0.1 | [console.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/console.py) | Update the console with methods allowing the user to create, destroy, show, and update stored data | +| 8. Create User class | [console.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/console.py) [/models/engine/file_storage.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/engine/file_storage.py) [/models/user.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/user.py) | Dynamically implements a user class | +| 9. More Classes | [/models/user.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/user.py) [/models/place.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/place.py) [/models/city.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/city.py) [/models/amenity.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/amenity.py) [/models/state.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/state.py) [/models/review.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/models/review.py) | Dynamically implements more classes | +| 10. Console 1.0 | [console.py](https://github.com/mwanzaalbert/AirBnB_clone_v2/blob/dev/console.py) [/models/engine/file_storage.py](https://github.com/justinmajetich/AirBnB_clone/blob/dev/models/engine/file_storage.py) | Update the console and file storage system to work dynamically with all classes update file storage |

General Use

@@ -139,4 +139,4 @@ Usage: .update(<_id>, ) (hbnb) User.all() (hbnb) ["[User] (98bea5de-9cb0-4d78-8a9d-c4de03521c30) {'updated_at': datetime.datetime(2020, 2, 19, 21, 47, 29, 134362), 'name': 'Fred the Frog', 'age': 9, 'id': '98bea5de-9cb0-4d78-8a9d-c4de03521c30', 'created_at': datetime.datetime(2020, 2, 19, 21, 47, 29, 134343)}"] ``` -
\ No newline at end of file +
From 0bf969a551fb4528155dd1ef74b86cd4d78d7b61 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Thu, 13 Feb 2025 07:13:46 +0300 Subject: [PATCH 081/101] refactor(console.py): refactor do create to allow creation of instances without kwargs when HBNB_TYPE_STORAGE != db --- console.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/console.py b/console.py index 8cbd6eac23a3..cbf6009fc78b 100755 --- a/console.py +++ b/console.py @@ -177,9 +177,10 @@ def do_create(self, args): " [=...]") else: - new_instance = HBNBCommand.classes[cls]() - new_instance.save() - print(new_instance.id) + if os.getenv('HBNB_TYPE_STORAGE') != 'db': + new_instance = HBNBCommand.classes[cls]() + new_instance.save() + print(new_instance.id) def help_create(self): """Help information for the create method.""" From 11c07c82949697a0d0e5298eefccbccc4c6b843a Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Thu, 13 Feb 2025 09:37:35 +0300 Subject: [PATCH 082/101] feat(console.py): added exception catching during instance creation --- console.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/console.py b/console.py index cbf6009fc78b..77b9e192188e 100755 --- a/console.py +++ b/console.py @@ -4,6 +4,7 @@ import sys import re import json +import sqlalchemy from models.base_model import BaseModel from models.user import User from models.place import Place @@ -168,19 +169,34 @@ def do_create(self, args): else: attr_dict[key] = val - new_instance = HBNBCommand.classes[cls](**attr_dict) - new_instance.save() - print(new_instance.id) + new_instance = HBNBCommand.classes[cls](**attr_dict) + print(new_instance.id) + + try: + new_instance.save() + + except sqlalchemy.exc.IntegrityError as e: + message = getattr(e, '_message') + message = message().partition(" ")[-1] + print(message.strip('()').split(',')[-1]) + storage.rollback() + else: print("Invalid param syntax. ") print("[Usage]: create =" + " [=...]") else: - if os.getenv('HBNB_TYPE_STORAGE') != 'db': - new_instance = HBNBCommand.classes[cls]() + new_instance = HBNBCommand.classes[cls]() + print(new_instance.id) + + try: new_instance.save() - print(new_instance.id) + except sqlalchemy.exc.IntegrityError as e: + message = getattr(e, '_message') + message = message().partition(" ")[-1] + print(message.strip('()').split(',')[-1]) + storage.rollback() def help_create(self): """Help information for the create method.""" From 054754008fcfddfd0b5b8a0bf449b0aa326bc1ab Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Thu, 13 Feb 2025 10:18:45 +0300 Subject: [PATCH 083/101] refactor(console.py: refactor do_create method to instantiate new_instance variable before passing kwargs) --- console.py | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/console.py b/console.py index 77b9e192188e..11895b37e86c 100755 --- a/console.py +++ b/console.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 """Console Module.""" +import os import cmd import sys import re @@ -130,6 +131,8 @@ def do_create(self, args): print("** class doesn't exist **") return + new_instance = HBNBCommand.classes[cls]() + if params: # substitute single quote for double to handle json parsing params = re.sub(r'[\']', '"', params) @@ -170,33 +173,15 @@ def do_create(self, args): attr_dict[key] = val new_instance = HBNBCommand.classes[cls](**attr_dict) - print(new_instance.id) - - try: - new_instance.save() - except sqlalchemy.exc.IntegrityError as e: - message = getattr(e, '_message') - message = message().partition(" ")[-1] - print(message.strip('()').split(',')[-1]) - storage.rollback() + # else: + # print("Invalid param syntax. ") + # print("[Usage]: create =" + + # " [=...]") - else: - print("Invalid param syntax. ") - print("[Usage]: create =" + - " [=...]") + print(new_instance.id) - else: - new_instance = HBNBCommand.classes[cls]() - print(new_instance.id) - - try: - new_instance.save() - except sqlalchemy.exc.IntegrityError as e: - message = getattr(e, '_message') - message = message().partition(" ")[-1] - print(message.strip('()').split(',')[-1]) - storage.rollback() + new_instance.save() def help_create(self): """Help information for the create method.""" From 21418f431158657397ee0df1a9c591a84f0553e2 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Fri, 14 Feb 2025 23:39:12 +0300 Subject: [PATCH 084/101] fix(models/user.py): fix columns erronously labelled nullable with False value to nullabe with True value --- models/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/user.py b/models/user.py index 9d022292e167..2fbff727b596 100644 --- a/models/user.py +++ b/models/user.py @@ -36,8 +36,8 @@ class User(BaseModel, Base): email = Column(String(128), nullable=False) password = Column(String(128), nullable=False) - first_name = Column(String(128), nullable=False) - last_name = Column(String(128), nullable=False) + first_name = Column(String(128), nullable=True) + last_name = Column(String(128), nullable=True) if os.getenv("HBNB_TYPE_STORAGE") == 'db': places = relationship('Place', From 7abc49cfe360ec6cc47ecf8ac36a01c24be822ea Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Fri, 14 Feb 2025 23:40:14 +0300 Subject: [PATCH 085/101] fix(models/place.py): fix columns erronously labelled nullable with False value to nullabe with True value --- models/place.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/place.py b/models/place.py index e39530dbd2c6..9b93e52200cb 100755 --- a/models/place.py +++ b/models/place.py @@ -64,13 +64,13 @@ class Place(BaseModel, Base): city_id = Column(String(60), ForeignKey('cities.id'), nullable=False) user_id = Column(String(60), ForeignKey('users.id'), nullable=False) name = Column(String(128), nullable=False) - description = Column(String(1024), nullable=False) + description = Column(String(1024), nullable=True) number_rooms = Column(Integer, nullable=False, default=0) number_bathrooms = Column(Integer, nullable=False, default=0) max_guest = Column(Integer, nullable=False, default=0) price_by_night = Column(Integer, nullable=False, default=0) - latitude = Column(Float, nullable=False, default=0.0) - longitude = Column(Float, nullable=False, default=0.0) + latitude = Column(Float, nullable=True, default=0.0) + longitude = Column(Float, nullable=True, default=0.0) if os.getenv("HBNB_TYPE_STORAGE") == 'db': reviews = relationship('Review', From 8497599d30eaafbf5d49a9d98bacab041f724cae Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Fri, 14 Feb 2025 23:54:33 +0300 Subject: [PATCH 086/101] fix(models/engine/db_storage.py): fix storage session reload when storage.all is called --- models/engine/db_storage.py | 56 +++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/models/engine/db_storage.py b/models/engine/db_storage.py index a2d749c566a0..aba76f8c80aa 100755 --- a/models/engine/db_storage.py +++ b/models/engine/db_storage.py @@ -4,7 +4,10 @@ DBStorage module for managing database storage in MySQL. This module defines the `DBStorage` class, which provides methods -to interact with a MySQL database using SQLAlchemy. +to interact with a MySQL database using SQLAlchemy. It handles +database connections, session management, and CRUD operations +for various models. + """ import os import importlib @@ -28,11 +31,16 @@ class DBStorage: """ - This class manages storage of hbnb models in a MySQL database. + Manages storage of hbnb models in a MySQL database. + + This class provides methods to interact with a MySQL database + using SQLAlchemy. It handles database connections, session + management, and CRUD operations for various models. Attributes_: __engine (sqlalchemy.engine.Engine): The database engine. __session (sqlalchemy.orm.scoped_session): The database session. + __objects (dict): A dictionary to store objects in memory. """ DB_CLASSES = [User, State, City, Amenity, Place, Review] @@ -81,7 +89,21 @@ def __init__(self): [-1].strip('()').split(',')[-1]) def all(self, cls=None): - """Return a dictionary of models currently in storage.""" + """ + Return a dictionary of models currently in storage. + + Args_: + cls (str or class, optional): The class or class name to filter + results. + If None, returns all objects. + + Returns_: + dict: A dictionary of objects, where the key is in the format + `.` and the value is the object + itself. + """ + self.reload() + if cls is not None: classes = ('BaseModel', 'User', @@ -121,24 +143,37 @@ def all(self, cls=None): return self.__objects def new(self, obj): - """Add new object to storage dictionary.""" + """ + Add a new object to the storage dictionary. + + Args_: + obj: The object to add to the session. + """ self.__session.add(obj) def save(self): - """Save storage dictionary to file.""" + """Commit the current session to the database.""" self.__session.commit() def reload(self): - """Load storage dictionary from file.""" + """ + Reload the storage dictionary from the database. + + This method creates all tables if they don't exist and initializes + a new session. + """ try: # Bind the engine to the Base's metadata Base.metadata.bind = self.__engine + # Create all tables defined in the Base Base.metadata.create_all(self.__engine) + # Create a session factory session_factory = sessionmaker(bind=self.__engine, expire_on_commit=False) + # Create a scoped session Session = scoped_session(session_factory) except OperationalError as e: @@ -149,7 +184,12 @@ def reload(self): self.__session = Session() def delete(self, obj=None): - """Delete object from the storage.""" + """ + Delete an object from the storage. + + Args_: + obj: The object to delete from the session. + """ if obj is not None: self.__session.delete(obj) @@ -162,7 +202,7 @@ def delete(self, obj=None): self.save() def close(self): - """Close the working SQLAlchemy session.""" + """Close the current SQLAlchemy session.""" self.__session.close() def rollback(self): From aaf1266a05e9e0dd0afa339463cbe2a832c64e21 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 11:48:58 +0300 Subject: [PATCH 087/101] test(tests/test_console.py): Add tests for missing non-nullable attributes for each model --- tests/test_console.py | 431 ++++++++++++++++++++++++++++++++---------- 1 file changed, 331 insertions(+), 100 deletions(-) diff --git a/tests/test_console.py b/tests/test_console.py index 84b21ca61521..671ff72c266c 100755 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -101,7 +101,7 @@ def setUp(self): def tearDown(self): """Clean up after each test""" # Clear the in-memory cache of objects - self.storage._FileStorage__objects.clear() + self.storage.all().clear() class TestCreateCommand(TestFileStorageConsole): @@ -544,7 +544,6 @@ def test_04_DBStorage__objects_not_empty_and_has_one_object(self): self.assertEqual(len(entries), 1) self.assertEqual(self.get_table_entries_count(), 1) self.assertIn(entries[-1], self.storage.all(self.model).keys()) - print(entries) def test_05_table_is_not_empty_and_has_one_entry(self): """Confirm the model's table is not empty""" @@ -559,7 +558,6 @@ def test_05_table_is_not_empty_and_has_one_entry(self): self.assertEqual(len(self.storage.all(self.model)), 1) self.assertEqual(len(entries), 1) self.assertEqual(self.get_table_entries_count(), 1) - print(entries) def test_07_DBStorage__objects_has_two_objects_of_the_model(self): """Confirm __objects size is greater thant 1""" @@ -576,7 +574,6 @@ def test_07_DBStorage__objects_has_two_objects_of_the_model(self): self.assertEqual(len(self.storage.all(self.model)), 2) self.assertEqual(len(entries), 2) self.assertEqual(self.get_table_entries_count(), 2) - print(entries) def test_08_table_has_two_entries(self): """Confirm users table has more than one row""" @@ -612,14 +609,11 @@ def test_09_deletion_of_last_table_entry(self): entry_id = key.split('.')[-1] - print("entries before delete", entries) # Refresh the MySQLdb connection to see the changes self.conn.commit() entries.pop(-1) - print("entries after delete", entries) - self.assertTrue(len(self.users) < initial_count, "Entry not deleted from the DB") self.assertEqual(self.get_table_entries_count(), @@ -650,8 +644,8 @@ def test_10_only_one_entry_exists_after_last_deletion(self): result = self.cursor.fetchone() self.assertTrue(result is not None) - def test_11_create_a_table_entry_without_attributes_values_pairs(self): - """Test that creating a User with missing attributes via the console + def test_11_create_a_table_entry_without_kwargs(self): + """Test that creating a record with missing attributes via the console does not add it to the database""" models = {'users': "User", 'places': "Place", @@ -662,11 +656,12 @@ def test_11_create_a_table_entry_without_attributes_values_pairs(self): } with patch("sys.stdout", new=StringIO()) as output: - with self.assertRaises(IntegrityError): - args = f'create {models[self.tablename]}' - HBNBCommand().onecmd(args) - # Roll back the session to clear the invalid state - self.storage.rollback() + args = f'create {models[self.tablename]}' + HBNBCommand().onecmd(args) + instance_id = output.getvalue().strip() + + self.assertEqual(instance_id, "") + self.assertFalse(instance_id) class Test_02_User(TestDBStorageConsole): @@ -678,7 +673,7 @@ class Test_02_User(TestDBStorageConsole): TestDBStorageConsole.create_attr_for_entries_created_based_table_name( tablename) - def test_03_create_one_user_with_valid_attributes(self): + def test_03_create_user_all_valid_attributes(self): """Test that creating a User via the console adds it to the database""" # Capture console output with patch("sys.stdout", new=StringIO()) as output: @@ -687,78 +682,67 @@ def test_03_create_one_user_with_valid_attributes(self): 'first_name="Guillaume" last_name="Snow"') user_id = output.getvalue().strip() - self.users.append("User." + user_id) + self.assertNotEqual(user_id, "", + "Failed to get User ID from console output") self.assertTrue(len(user_id) > 0, "Failed to get User ID from console output") self.assertRegex(user_id, r'^[0-9a-f-]{36}$') + self.users.append("User." + user_id) - def test_06_create_additional_user_with_valid_attributes(self): - """Test that creating a User via the console adds it to the database""" - # Capture console output + def test_06_create_user_missing_nullable_attributes(self): + """Test that creating a User with missing nullable attributes via the + console does not add it to the database""" + # Test user creation missing last_name & first_name attributes with patch("sys.stdout", new=StringIO()) as output: - HBNBCommand().onecmd( - 'create User email="johndoe@example.com" ' + - 'password="password" first_name="John" last_name="Doe"') + args = 'create User email="a@a.com" password="pwd"' + HBNBCommand().onecmd(args) + user_id = output.getvalue().strip() - self.users.append("User." + user_id) + self.assertNotEqual(user_id, "", + "Failed to get User ID from console output") self.assertTrue(len(user_id) > 0, "Failed to get User ID from console output") self.assertRegex(user_id, r'^[0-9a-f-]{36}$') + self.users.append("User." + user_id) - def test_12_create_user_with_single_attribute_value_pair(self): - """Test that creating a User with missing attributes via the console - does not add it to the database""" - # Test missing email attribute - with patch("sys.stdout", new=StringIO()) as output: - with self.assertRaises(IntegrityError): - args = 'create User first_name="Guillaume"' - HBNBCommand().onecmd(args) - # Roll back the session to clear the invalid state - self.storage.rollback() - - def test_13_create_user_with_missing_attribute_value_pairs(self): - """Test that creating a User with missing attributes via the console - does not add it to the database""" - # Test missing email attribute + def test_13_create_user_missing_non_nullable_attributes(self): + """ + Test that creating a User via the console with missing non_nullable + attributes does not add it to the database + """ + # Test missing email and password attributes with patch("sys.stdout", new=StringIO()) as output: - with self.assertRaises(IntegrityError): - args = 'create User password="guipwd" ' +\ - 'first_name="Guillaume" last_name="Snow"' - HBNBCommand().onecmd(args) - # Roll back the session to clear the invalid state - self.storage.rollback() + args = 'create User first_name="Guillaume" last_name="Snow"' + HBNBCommand().onecmd(args) + user_id = output.getvalue().strip() + + self.assertEqual(user_id, "") + self.assertFalse(user_id) # Test missing password attribute with patch("sys.stdout", new=StringIO()) as output: - with self.assertRaises(IntegrityError): - args = 'create User email="gui@hbtn.io" ' +\ - 'first_name="Guillaume" last_name="Snow"' - HBNBCommand().onecmd(args) - # Roll back the session to clear the invalid state - self.storage.rollback() - - # Test missing first_name attribute - with patch("sys.stdout", new=StringIO()) as output: - with self.assertRaises(IntegrityError): - args = 'create User email="gui@hbtn.io" ' +\ - 'password="guipwd" last_name="Snow"' - HBNBCommand().onecmd(args) - # Roll back the session to clear the invalid state - self.storage.rollback() - - # Test missing last_name attribute + args = 'create User email="a@a.com"' + HBNBCommand().onecmd(args) + user_id = output.getvalue().strip() + + self.assertEqual(user_id, "") + self.assertFalse(user_id) + + # Test missing email attribute with patch("sys.stdout", new=StringIO()) as output: - with self.assertRaises(IntegrityError): - args = 'create User email="gui@hbtn.io" ' +\ - 'password="guipwd" first_name="Guillaume"' - HBNBCommand().onecmd(args) - # Roll back the session to clear the invalid state - self.storage.rollback() + args = 'create User password="pwd"' + HBNBCommand().onecmd(args) + user_id = output.getvalue().strip() + + self.assertEqual(user_id, "") + self.assertFalse(user_id) class Test_03_State(TestDBStorageConsole): - """Tests DBStorage integration with console commands for the State model""" + """ + Tests DBStorage integration with console commands for the State model + """ tablename = "states" model = TestDBStorageConsole.get_model_class(tablename) @@ -766,17 +750,19 @@ class Test_03_State(TestDBStorageConsole): TestDBStorageConsole.create_attr_for_entries_created_based_table_name( tablename) - def test_03_create_one_state_with_valid_attributes(self): + def test_03_create_state_valid_attributes(self): """Test that creating a User via the console adds it to the database""" # Capture console output with patch("sys.stdout", new=StringIO()) as output: HBNBCommand().onecmd('create State name="California"') state_id = output.getvalue().strip() - self.states.append("State." + state_id) + self.assertNotEqual(state_id, "", + "Failed to get State ID from console output") self.assertTrue(len(state_id) > 0, "Failed to get State ID from console output") self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + self.states.append("State." + state_id) def test_06_create_additional_state_with_valid_attributes(self): """Test that creating a User via the console adds it to the database""" @@ -785,14 +771,18 @@ def test_06_create_additional_state_with_valid_attributes(self): HBNBCommand().onecmd('create State name="Nevada"') state_id = output.getvalue().strip() - self.states.append("State." + state_id) + self.assertNotEqual(state_id, "", + "Failed to get State ID from console output") self.assertTrue(len(state_id) > 0, "Failed to get State ID from console output") self.assertRegex(state_id, r'^[0-9a-f-]{36}$') + self.states.append("State." + state_id) class Test_04_City(TestDBStorageConsole): - """Tests DBStorage integration with console commands for the City model""" + """ + Tests DBStorage integration with console commands for the City model + """ tablename = "cities" model = TestDBStorageConsole.get_model_class(tablename) @@ -800,8 +790,10 @@ class Test_04_City(TestDBStorageConsole): TestDBStorageConsole.create_attr_for_entries_created_based_table_name( tablename) - def test_03_create_one_city_with_valid_attributes(self): - """Test that creating a City via the console adds it to the database""" + def test_03_create_city_existing_state_id(self): + """ + Test that creating a City via the console adds it to the database + """ self.assertNotEqual(self.get_table_entries_count( "states"), 0, "states table is empty") @@ -819,13 +811,17 @@ def test_03_create_one_city_with_valid_attributes(self): HBNBCommand().onecmd(arg) city_id = output.getvalue().strip() - self.cities.append("City." + city_id) + self.assertNotEqual(city_id, "", + "Failed to get City ID from console output") self.assertTrue(len(city_id) > 0, "Failed to get City ID from console output") self.assertRegex(city_id, r'^[0-9a-f-]{36}$') + self.cities.append("City." + city_id) - def test_06_create_additional_city_with_valid_attributes(self): - """Test that creating a City via the console adds it to the database""" + def test_06_create_city_all_existing_attributes(self): + """ + Test that creating a City via the console adds it to the database + """ initial_state_count = self.get_table_entries_count("states") # 1. Create a new State first since City requires a valid state_id @@ -833,6 +829,8 @@ def test_06_create_additional_city_with_valid_attributes(self): HBNBCommand().onecmd('create State name="Nevada"') state_id = output.getvalue().strip() + self.assertNotEqual(state_id, "", + "Failed to get State ID from console output") self.assertTrue(len(state_id) > 0, "Failed to get State ID from console output") self.assertRegex(state_id, r'^[0-9a-f-]{36}$') @@ -849,10 +847,38 @@ def test_06_create_additional_city_with_valid_attributes(self): f'create City name="Reno" state_id="{state_id}"') city_id = output.getvalue().strip() - self.cities.append("City." + city_id) + self.assertNotEqual(city_id, "", + "Failed to get City ID from console output") self.assertTrue(len(city_id) > 0, "Failed to get City ID from console output") self.assertRegex(city_id, r'^[0-9a-f-]{36}$') + self.cities.append("City." + city_id) + + def test_12_create_city_non_existent_state_id(self): + """ + Test that creating a City via the console with non_existent state_id + does not add it to the database + """ + with patch("sys.stdout", new=StringIO()) as output: + args = 'create City name="Fremont" state_id="12345"' + HBNBCommand().onecmd(args) + city_id = output.getvalue().strip() + + self.assertEqual(city_id, "") + self.assertFalse(city_id) + + def test_13_create_city_missing_state_id(self): + """ + Test that creating a City via the console with missing state_id + does not add it to the database + """ + with patch("sys.stdout", new=StringIO()) as output: + args = 'create City name="Fremont"' + HBNBCommand().onecmd(args) + city_id = output.getvalue().strip() + + self.assertEqual(city_id, "") + self.assertFalse(city_id) class Test_05_Place(TestDBStorageConsole): @@ -864,7 +890,7 @@ class Test_05_Place(TestDBStorageConsole): TestDBStorageConsole.create_attr_for_entries_created_based_table_name( tablename) - def test_03_create_one_place_with_valid_attributes(self): + def test_03_create_place_all_existing_attributes(self): """ Test that creating a Place via the console adds it to the database. """ @@ -899,14 +925,17 @@ def test_03_create_one_place_with_valid_attributes(self): HBNBCommand().onecmd(arg) place_id = output.getvalue().strip() - self.places.append("Place." + place_id) + self.assertNotEqual(place_id, "", + "Failed to get Place ID from console output") self.assertTrue(len(place_id) > 0, "Failed to get Place ID from console output") self.assertRegex(place_id, r'^[0-9a-f-]{36}$') + self.places.append("Place." + place_id) - def test_06_create_additional_place_with_valid_attributes(self): + def test_06_create_place_missing_nullable_and_default_attributes(self): """ - Test that creating a Place via the console adds it to the database. + Test that creating a Place via the console with + missing_nullable_and_default_attributes adds it to the database. """ initial_state_count = self.get_table_entries_count("states") initial_user_count = self.get_table_entries_count("users") @@ -959,18 +988,115 @@ def test_06_create_additional_place_with_valid_attributes(self): # 3. Create a Place using the user_id and city_id with patch("sys.stdout", new=StringIO()) as output: - arg = f'create Place city_id="{city_id}" user_id="{user_id}" ' +\ - 'name="Lovely_place" description="No_description_provided"' +\ - ' number_rooms=3 number_bathrooms=1 ' +\ - 'max_guest=6 price_by_night=120 latitude=-37.773972 ' +\ - 'longitude=122.431297' + arg = f'create Place city_id="{city_id}" ' +\ + f'user_id="{user_id}" name="Lovely_place"' HBNBCommand().onecmd(arg) place_id = output.getvalue().strip() - self.places.append("Place." + place_id) + self.assertNotEqual(place_id, "", + "Failed to get Place ID from console output") self.assertTrue(len(place_id) > 0, "Failed to get Place ID from console output") self.assertRegex(place_id, r'^[0-9a-f-]{36}$') + self.places.append("Place." + place_id) + + def test_12_create_place_missing_user_id(self): + """ + Test that creating a Place via the console with missing user_id + doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "cities"), 0, "cities table is empty") + self.assertTrue(self.get_table_entries_count( + "cities") >= 1, "cities table is empty") + + # 1. Get an existing city_id + query = 'SELECT id from cities WHERE name=%s;' + self.cursor.execute(query, ("San Francisco",)) + city_id = self.cursor.fetchone()[0] + + # 2. Create a Place without a user_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="{city_id}" name="Wonderful_place"' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.assertEqual(place_id, "") + self.assertFalse(place_id) + + def test_13_create_place_non_existent_user_id(self): + """ + Test that creating a Place via the console with non_existent user_id + doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "cities"), 0, "cities table is empty") + self.assertTrue(self.get_table_entries_count( + "cities") >= 1, "cities table is empty") + + # 1. Get an existing city_id + query = 'SELECT id from cities WHERE name=%s;' + self.cursor.execute(query, ("San Francisco",)) + city_id = self.cursor.fetchone()[0] + + # 2. Create a Place with a non-existent user_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="{city_id}" user_id="12345" ' +\ + 'name="Wonderful_place"' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.assertEqual(place_id, "") + self.assertFalse(place_id) + + def test_14_create_place_missing_city_id(self): + """ + Test that creating a Place via the console with missing city_id + doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + # 2. Get an existing user_id + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + + # 3. Create a Place without a city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place user_id="{user_id}" name="Joyful_place"' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.assertEqual(place_id, "") + self.assertFalse(place_id) + + def test_15_create_place_non_existent_city_id(self): + """ + Test that creating a Place via the console with non-existent city_id + doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + # 2. Get an existing user_id + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + + # 3. Create a Place without a city_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Place city_id="12345" user_id="{user_id}" ' +\ + 'name="Joyful_place"' + HBNBCommand().onecmd(arg) + place_id = output.getvalue().strip() + + self.assertEqual(place_id, "") + self.assertFalse(place_id) class Test_06_Review(TestDBStorageConsole): @@ -984,9 +1110,9 @@ class Test_06_Review(TestDBStorageConsole): TestDBStorageConsole.create_attr_for_entries_created_based_table_name( tablename) - def test_03_create_one_place_with_valid_attributes(self): + def test_03_create_review_all_existing_attributes(self): """ - Test that creating a Place via the console adds it to the database. + Test that creating a Review via the console adds it to the database. """ self.assertNotEqual(self.get_table_entries_count( @@ -1014,16 +1140,18 @@ def test_03_create_one_place_with_valid_attributes(self): # 3. Create a Review using the user_id and city_id with patch("sys.stdout", new=StringIO()) as output: arg = f'create Review place_id="{place_id}" user_id="{user_id}"' +\ - ' text="Amazing_place,_huge_kitchen"' + ' text="Amazing_place,_beautiful_beach"' HBNBCommand().onecmd(arg) review_id = output.getvalue().strip() - self.reviews.append("Review." + review_id) + self.assertNotEqual(review_id, "", + "Failed to get Review ID from console output") self.assertTrue(len(review_id) > 0, "Failed to get Review ID from console output") self.assertRegex(review_id, r'^[0-9a-f-]{36}$') + self.reviews.append("Review." + review_id) - def test_06_create_additional_place_with_valid_attributes(self): + def test_06_create_review_all_valid_attributes(self): """ Test that creating a Place via the console adds it to the database. """ @@ -1067,10 +1195,7 @@ def test_06_create_additional_place_with_valid_attributes(self): # Create a Place using the user_id and city_id with patch("sys.stdout", new=StringIO()) as output: arg = f'create Place city_id="{city_id}" user_id="{user_id}" ' +\ - 'name="Lovely_place" description="No_description_provided" ' +\ - 'number_rooms=3 number_bathrooms=1 max_guest=6 ' +\ - 'price_by_night=120 latitude=-37.773972 ' +\ - 'longitude=122.431297' + 'name="Lovely_place"' HBNBCommand().onecmd(arg) place_id = output.getvalue().strip() @@ -1102,6 +1227,108 @@ def test_06_create_additional_place_with_valid_attributes(self): "Failed to get Review ID from console output") self.assertRegex(review_id, r'^[0-9a-f-]{36}$') + def test_12_create_review_missing_user_id(self): + """ + Test that creating a Review via the console with missing + user_id doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "places"), 0, "places table is empty") + self.assertTrue(self.get_table_entries_count( + "places") >= 1, "places table is empty") + + # 1. Get a existing place_id + query = 'SELECT id from places WHERE name=%s;' + self.cursor.execute(query, ("Happy place",)) + place_id = self.cursor.fetchone()[0] + + # 3. Create a Review without a user_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review place_id="{place_id}" ' +\ + 'text="Amazing_place,_beautiful_beach"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.assertEqual(review_id, "") + self.assertFalse(review_id) + + def test_13_create_review_non_existent_user_id(self): + """ + Test that creating a Review via the console with non_existent user_id + doesn't add it to the database. + """ + self.assertNotEqual(self.get_table_entries_count( + "places"), 0, "places table is empty") + self.assertTrue(self.get_table_entries_count( + "places") >= 1, "places table is empty") + + # 1. Get a existing place_id + query = 'SELECT id from places WHERE name=%s;' + self.cursor.execute(query, ("Happy place",)) + place_id = self.cursor.fetchone()[0] + + # 3. Create a Review with a non-existent user_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review place_id="{place_id}" user_id="12345" ' +\ + 'text="Amazing_place,_beautiful_beach"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.assertEqual(review_id, "") + self.assertFalse(review_id) + + def test_14_create_review_missing_place_id(self): + """ + Test that creating a Review via the console with missing + place_id doesn't add it to the database + """ + + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + # 1. Get an exisiting user_id + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + + # 2. Create a Review without a place_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review user_id="{user_id}" ' +\ + 'text="Amazing_place,_beautiful_beach"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.assertEqual(review_id, "") + self.assertFalse(review_id) + + def test_15_create_review_non_existent_place_id(self): + """ + Test that creating a Review via the console with non_existent + place_id doesn't add it to the database. + """ + + self.assertNotEqual(self.get_table_entries_count( + "users"), 0, "users table is empty") + self.assertTrue(self.get_table_entries_count( + "users") >= 1, "users table is empty") + + # 1. Get an exisiting user_id + query = 'SELECT id from users WHERE last_name=%s;' + self.cursor.execute(query, ("Snow",)) + user_id = self.cursor.fetchone()[0] + + # 2. Create a Review with non-existent place_id + with patch("sys.stdout", new=StringIO()) as output: + arg = f'create Review place_id="12345" user_id="{user_id}" ' +\ + 'text="Amazing_place,_beautiful_beach"' + HBNBCommand().onecmd(arg) + review_id = output.getvalue().strip() + + self.assertEqual(review_id, "") + self.assertFalse(review_id) + class Test_07_Amenity(TestDBStorageConsole): """ @@ -1114,8 +1341,10 @@ class Test_07_Amenity(TestDBStorageConsole): TestDBStorageConsole.create_attr_for_entries_created_based_table_name( tablename) - def test_03_create_one_amenity_with_valid_attributes(self): - """Test that creating a User via the console adds it to the database""" + def test_03_create_amenity_with_valid_attributes(self): + """ + Test that creating an Amenity via the console adds it to the database + """ # Capture console output with patch("sys.stdout", new=StringIO()) as output: HBNBCommand().onecmd('create Amenity name="Wifi"') @@ -1127,7 +1356,9 @@ def test_03_create_one_amenity_with_valid_attributes(self): self.assertRegex(amenity_id, r'^[0-9a-f-]{36}$') def test_06_create_additional_amenity_with_valid_attributes(self): - """Test that creating a User via the console adds it to the database""" + """ + Test that creating an Amenity via the console adds it to the database + """ # Capture console output with patch("sys.stdout", new=StringIO()) as output: HBNBCommand().onecmd('create Amenity name="Oven"') From 2185647bf7e8d40c68aad95297f3b0b76d20d51d Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 14:22:12 +0300 Subject: [PATCH 088/101] refactor(tests/test_models/test_engine/test_db_storage.py): refactor amenity_place_relationship test, user_creation_missing_kwargs test --- .../test_engine/test_db_storage.py | 63 ++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/tests/test_models/test_engine/test_db_storage.py b/tests/test_models/test_engine/test_db_storage.py index e6badee9f2fe..864ffecefd79 100755 --- a/tests/test_models/test_engine/test_db_storage.py +++ b/tests/test_models/test_engine/test_db_storage.py @@ -196,11 +196,12 @@ def test_02_record_creation_without_kwargs(self): the database""" self.create_a_new_record_instance() - with self.assertRaises(sqlalchemy.exc.IntegrityError): + try: self.new_record.save() - - # Roll back the session to clear the invalid state - self.storage.rollback() + except sqlalchemy.exc.IntegrityError: + self.storage.rollback() # clear the invalid state + finally: + self.assert_object_not_in_db(self.new_record) def test_04_table_creation_valid_kwargs(self): """Test creating and saving a new_record object to the database""" @@ -304,27 +305,31 @@ def test_07_user_update(self): self.assertIsNotNone(result, "User not updated") def test_08_user_creation_missing_kwargs(self): + # missing password new_user1 = self.model(email="johndoe@example.com", first_name="John", last_name="Doe") - + # missing email new_user2 = self.model(password="password", first_name="John", last_name="Doe") - + # missing first_name new_user3 = self.model(email="bond@doubleoseven.com", password="password", last_name="bond") - + # missing last_name new_user4 = self.model(email="test@example.com", password="password", first_name="John") for user in (new_user1, new_user2, new_user3, new_user4): - with self.assertRaises(sqlalchemy.exc.IntegrityError): + try: user.save() - # Roll back the session to clear the invalid state - self.storage.rollback() + except sqlalchemy.exc.IntegrityError: + self.storage.rollback() # Clear the invalid state + self.assert_object_not_in_db(user) + else: + self.assert_object_in_db(user) class Test_09_State(BaseTableTests): @@ -698,8 +703,38 @@ def test_07_amenity_update(self): def test_08_place_amenities_relationship(self): """Test the relationship between City, User and State""" - place_id = self.new_place.id - self.new_place.amenities.append(self.new_record) + new_user = User( + email="sales@hpconfectionaries.com", + password="sweethoney", + first_name="Dwight", + last_name="Malone", + ) + + new_state = State(name="Oklahoma") + + new_city = City(name="Tulsa", state_id=new_state.id) + + new_place = Place(name="Higher Plane", + city_id=new_city.id, + user_id=new_user.id) + + new_review = Review(place_id=new_place.id, + user_id=new_user.id, + text="I love the weed edibles!") + + new_amenity = Amenity(name="Meditation Lounge") + + self.storage.new(new_user) + self.storage.new(new_state) + self.storage.new(new_city) + self.storage.new(new_place) + self.storage.new(new_review) + self.storage.new(new_amenity) + self.storage.save() + + place_id = new_place.id + + new_place.amenities.append(new_amenity) self.storage.save() @@ -715,8 +750,8 @@ def test_08_place_amenities_relationship(self): amenity, amenity_id = result - self.assertEqual(self.new_record.name, amenity) - self.assertEqual(self.new_record.id, amenity_id) + self.assertEqual(new_amenity.name, amenity) + self.assertEqual(new_amenity.id, amenity_id) if __name__ == '__main__': From f34977d821f0abd2d9c87c8936d9f2a0b418249d Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 14:38:45 +0300 Subject: [PATCH 089/101] refactor(console.py): refactor do_create handle exceptions, do_update to evaluate attribute value using json, precmd to replace single quotes with double quotes for json parsing, do_update to evaluate dictionary represtation using ast.literal_eval, do_destroy to properly delete an object instance --- console.py | 273 +++++++++++++++++++++++++++-------------------------- 1 file changed, 140 insertions(+), 133 deletions(-) diff --git a/console.py b/console.py index 11895b37e86c..110ce1325c67 100755 --- a/console.py +++ b/console.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 """Console Module.""" -import os import cmd +import ast import sys import re import json @@ -30,12 +30,6 @@ class HBNBCommand(cmd.Cmd): dot_cmds = ['all', 'count', 'show', 'destroy', 'update'] - types = { - 'number_rooms': int, 'number_bathrooms': int, - 'max_guest': int, 'price_by_night': int, - 'latitude': float, 'longitude': float - } - def preloop(self): """Print if isatty is false.""" if not sys.__stdin__.isatty(): @@ -47,46 +41,53 @@ def precmd(self, line): Usage: .([ [<*args> or <**kwargs>]]) (Brackets denote optional fields in usage example.) """ + # substitute single quote for double to handle json parsing + line = re.sub(r'[\']', '"', line[:]) + _cmd = _cls = _id = _args = '' # initialize line elements # scan for general formating - i.e '.', '(', ')' - if not ('.' in line and '(' in line and ')' in line): + if not all(char in line for char in {'.', '(', ')'}): + # if not ('.' in line and '(' in line and ')' in line): return line try: # parse line left to right - pline = line[:] # parsed line + parsed_line = line[:] # parsed line # isolate - _cls = pline[:pline.find('.')] + _cls = parsed_line[:parsed_line.find('.')] # isolate and validate - _cmd = pline[pline.find('.') + 1:pline.find('(')] + _cmd = parsed_line[parsed_line.find('.') + 1:parsed_line.find('(')] + if _cmd not in HBNBCommand.dot_cmds: raise Exception # if parantheses contain arguments, parse them - pline = pline[pline.find('(') + 1:pline.find(')')] - if pline: + args = parsed_line[parsed_line.find('(') + 1:parsed_line.find(')')] + if args: # partition args: (, [], [<*args>]) - pline = pline.partition(', ') # pline convert to tuple + _id, _, attr_name_and_value = args.partition(', ') +# pline = pline.partition(', ') # pline convert to tuple - # isolate _id, stripping quotes - _id = pline[0].replace('\"', '') + # strip leading quotes and spaces + _id = _id.strip('\'" ') # possible bug here: # empty quotes register as empty _id when replaced + # strip leading quotes and spaces + attr_name_and_value = attr_name_and_value.strip('\'" ') + # if arguments exist beyond _id - pline = pline[2].strip() # pline is now str - if pline: + if attr_name_and_value: # check for *args or **kwargs - if pline[0] == '{' and pline[-1] == '}'\ - and type(eval(pline)) is dict: - _args = pline + if attr_name_and_value.startswith("{") or\ + attr_name_and_value.endswith("}"): + _args = attr_name_and_value else: - _args = pline.replace(',', '') - # _args = _args.replace('\"', '') - line = ' '.join([_cmd, _cls, _id, _args]) + _args = attr_name_and_value.replace(',', '') + line = ' '.join([_cmd, _cls, _id, _args]) except Exception as mess: pass @@ -95,11 +96,11 @@ def precmd(self, line): def postcmd(self, stop, line): """Prints if isatty is false.""" - if not sys.__stdin__.isatty(): + if not sys.__stdin__.isatty() and line.strip() != "": print('(hbnb) ', end='') return stop - def do_quit(self, command): + def do_quit(self, line): """Method to exit the HBNB console.""" return True @@ -107,7 +108,7 @@ def help_quit(self): """Prints the help documentation for quit.""" print("Exits the program with formatting\n") - def do_EOF(self, arg): + def do_EOF(self, line): """Handles EOF to exit program.""" return True @@ -134,11 +135,6 @@ def do_create(self, args): new_instance = HBNBCommand.classes[cls]() if params: - # substitute single quote for double to handle json parsing - params = re.sub(r'[\']', '"', params) - - # patterns for the attributes' values in the - # param syntax = strings = r"""["'](?P[A-Za-z0-9_,!@#$%^&*\.-]+)["']""" floats = r"(?P(?:[-]?[0-9]+(?=\.))(?:\.[0-9]*(?![\.0-9-]+))" ints = r"(?P[-]?[0-9]+(?=\s))" @@ -172,82 +168,79 @@ def do_create(self, args): else: attr_dict[key] = val - new_instance = HBNBCommand.classes[cls](**attr_dict) - - # else: - # print("Invalid param syntax. ") - # print("[Usage]: create =" + - # " [=...]") - - print(new_instance.id) + new_instance = HBNBCommand.classes[cls](**attr_dict) - new_instance.save() + try: + new_instance.save() + except sqlalchemy.exc.IntegrityError: + storage.rollback() + return + else: + print(new_instance.id) def help_create(self): """Help information for the create method.""" print("Creates a class of any type") print("[Usage]: create \n") + print("[Usage]: create =" + + " [=...]\n") - def do_show(self, args): + def do_show(self, line): """Method to show an individual object.""" - new = args.partition(" ") - c_name = new[0] - c_id = new[2] + cls, _, instance_id = line.partition(" ") - # guard against trailing args - if c_id and ' ' in c_id: - c_id = c_id.partition(' ')[0] - - if not c_name: + if not cls: print("** class name missing **") return - if c_name not in HBNBCommand.classes: + if cls not in HBNBCommand.classes: print("** class doesn't exist **") return - if not c_id: + if not instance_id: print("** instance id missing **") return - key = c_name + "." + c_id - try: - print(storage.all()[key]) - except KeyError: + instance_id = instance_id.strip('\'" ') + + key = f"{cls}.{instance_id}" + + if key not in storage.all().keys(): print("** no instance found **") + return + + print(storage.all()[key]) def help_show(self): """Help information for the show command.""" print("Shows an individual instance of a class") print("[Usage]: show \n") - def do_destroy(self, args): + def do_destroy(self, line): """Destroy a specified object.""" - new = args.partition(" ") - c_name = new[0] - c_id = new[2] - if c_id and ' ' in c_id: - c_id = c_id.partition(' ')[0] + cls, _, instance_id = line.partition(" ") - if not c_name: + if not cls: print("** class name missing **") return - if c_name not in HBNBCommand.classes: + if cls not in HBNBCommand.classes: print("** class doesn't exist **") return - if not c_id: + if not instance_id: print("** instance id missing **") return - key = c_name + "." + c_id + instance_id = instance_id.strip('\'" ') - try: - del (storage.all()[key]) - storage.save() - except KeyError: + key = f"{cls}.{instance_id}" + + if key not in storage.all().keys(): print("** no instance found **") + return + + storage.delete(storage.all().copy()[key]) def help_destroy(self): """Help information for the destroy command.""" @@ -260,9 +253,11 @@ def do_all(self, args): if args: cls = args.split(' ')[0] # remove possible trailing args + if cls not in HBNBCommand.classes: print("** class doesn't exist **") return + for key, value in storage.all().items(): if key.split('.')[0] == cls: print_list.append(str(value)) @@ -277,13 +272,22 @@ def help_all(self): print("Shows all objects, or all of a class") print("[Usage]: all \n") - def do_count(self, args): + def do_count(self, line): """Count current number of class instances.""" - storage.reload() # added Explicitly reload storage + + cls, _, _ = line.partition(" ") + + if not cls: + print("** class name missing **") + return + + if cls not in HBNBCommand.classes: + print("** class doesn't exist **") + return count = 0 - for k, v in storage.all().items(): - if args == k.split('.')[0]: + for key in storage.all(): + if cls == key.split('.')[0]: count += 1 print(count) @@ -292,88 +296,91 @@ def help_count(self): """Help information for the count command.""" print("Usage: count ") - def do_update(self, args): + def do_update(self, line): """Update a certain object with new info.""" - c_name = c_id = att_name = att_val = kwargs = '' + # isolate cls from id/args + cls, _, args = line.strip().partition(" ") - # isolate cls from id/args, ex: (, delim, ) - args = args.partition(" ") - if args[0]: - c_name = args[0] - else: # class name not present + if not cls: print("** class name missing **") return - if c_name not in HBNBCommand.classes: # class name invalid + + if cls not in HBNBCommand.classes: # class name invalid print("** class doesn't exist **") return # isolate id from args - args = args[2].partition(" ") - if args[0]: - c_id = args[0] - else: # id not present + instance_id, _, args_or_kwargs = args.strip().partition(" ") + instance_id = instance_id.strip("\"' ") + args_or_kwargs = args_or_kwargs.strip("\"' ") + + if not instance_id: print("** instance id missing **") return # generate key from class and id - key = c_name + "." + c_id + key = f"{cls}.{instance_id}" # determine if key is present if key not in storage.all(): print("** no instance found **") return - # first determine if kwargs or args - if '{' in args[2] and '}' in args[2] and type(eval(args[2])) is dict: - kwargs = eval(args[2]) - args = [] # reformat kwargs into list, ex: [, , ...] - for k, v in kwargs.items(): - args.append(k) - args.append(v) - else: # isolate args - args = args[2] - if args and args[0] == '\"': # check for quoted arg - second_quote = args.find('\"', 1) - att_name = args[1:second_quote] - args = args[second_quote + 1:] - - args = args.partition(' ') - - # if att_name was not quoted arg - if not att_name and args[0] != ' ': - att_name = args[0] - # check for quoted val arg - if args[2] and args[2][0] == '\"': - att_val = args[2][1:args[2].find('\"', 1)] - - # if att_val was not quoted arg - if not att_val and args[2]: - att_val = args[2].partition(' ')[0] - - args = [att_name, att_val] - - # retrieve dictionary of current objects - new_dict = storage.all()[key] - - # iterate through attr names and values - for i, att_name in enumerate(args): - # block only runs on even iterations - if (i % 2 == 0): - att_val = args[i + 1] # following item is value - if not att_name: # check for att_name - print("** attribute name missing **") + # Retrieve object to update its attributes + updated_obj = storage.all()[key] + + if args_or_kwargs: + if any(char in args_or_kwargs for char in '{}'): + # Handle dictionary representation + try: + # Safely evaluates Python dictionary-like strings + attr_dict = ast.literal_eval(args_or_kwargs.strip("\"' ")) + if not isinstance(attr_dict, dict): + raise ValueError + except (SyntaxError, ValueError): + print("** invalid dictionary representation **") return - if not att_val: # check for att_value + else: + for attr_name, attr_value in attr_dict.items(): + attr_name = attr_name.strip("\"' ") + + if isinstance(attr_value, str): + attr_value = attr_value.strip("\"' ") + + setattr(updated_obj, attr_name, attr_value) + + else: + attr_name, _, attr_value = args_or_kwargs.partition(" ") + + if not attr_value: print("** value missing **") return - # type cast as necessary - if att_name in HBNBCommand.types: - att_val = HBNBCommand.types[att_name](att_val) - # update dictionary with name, value pair - new_dict.__dict__.update({att_name: att_val}) + attr_name = attr_name.strip("\"' ") + + attr_value = attr_value.strip("\"' ") + + # guard against trailing args + attr_value, _, xtra_args = attr_value.partition(" ") + + attr_value = attr_value.strip("\"' ") + + if not xtra_args: + try: + # Convert to appropriate type if possible + attr_value = json.loads(attr_value) + except (json.JSONDecodeError): + pass # Keep as string if evaluation fails + except Exception as e: + pass + finally: + setattr(updated_obj, attr_name, attr_value) + + else: + print("** attribute name missing **") + return - new_dict.save() # save updates to file + updated_obj.save() # Save the updated object def help_update(self): """Help information for the update class.""" From 658376fe852ac3a1bec2f0e39f3df6d09af8c7c3 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 15:42:26 +0300 Subject: [PATCH 090/101] refactor: remove error catching from do_create --- console.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/console.py b/console.py index 110ce1325c67..9aa21c3b1aaf 100755 --- a/console.py +++ b/console.py @@ -170,13 +170,8 @@ def do_create(self, args): new_instance = HBNBCommand.classes[cls](**attr_dict) - try: - new_instance.save() - except sqlalchemy.exc.IntegrityError: - storage.rollback() - return - else: - print(new_instance.id) + new_instance.save() + print(new_instance.id) def help_create(self): """Help information for the create method.""" From 1fb1316e4d3d1bd15b060f7b898b1c7f030b39ef Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 19:21:08 +0300 Subject: [PATCH 091/101] refactor: Add error catching --- console.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/console.py b/console.py index 9aa21c3b1aaf..110ce1325c67 100755 --- a/console.py +++ b/console.py @@ -170,8 +170,13 @@ def do_create(self, args): new_instance = HBNBCommand.classes[cls](**attr_dict) - new_instance.save() - print(new_instance.id) + try: + new_instance.save() + except sqlalchemy.exc.IntegrityError: + storage.rollback() + return + else: + print(new_instance.id) def help_create(self): """Help information for the create method.""" From d00badd29e01f26ddfc3a9aec8934cbb5ad2f1b3 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 20:48:51 +0300 Subject: [PATCH 092/101] refactor: refactor precmd, do_update to v2.0.0 implementation --- console.py | 338 ++++++++++++++++++++++++++--------------------------- 1 file changed, 166 insertions(+), 172 deletions(-) diff --git a/console.py b/console.py index 110ce1325c67..1fdefe78c537 100755 --- a/console.py +++ b/console.py @@ -1,11 +1,9 @@ #!/usr/bin/python3 -"""Console Module.""" +""" Console Module """ import cmd -import ast import sys import re import json -import sqlalchemy from models.base_model import BaseModel from models.user import User from models.place import Place @@ -17,21 +15,26 @@ class HBNBCommand(cmd.Cmd): - """Contains the functionality for the HBNB console""" + """ Contains the functionality for the HBNB console""" # determines prompt for interactive/non-interactive modes prompt = '(hbnb) ' if sys.__stdin__.isatty() else '' classes = { - 'BaseModel': BaseModel, 'User': User, 'Place': Place, - 'State': State, 'City': City, 'Amenity': Amenity, - 'Review': Review - } - + 'BaseModel': BaseModel, 'User': User, 'Place': Place, + 'State': State, 'City': City, 'Amenity': Amenity, + 'Review': Review + } dot_cmds = ['all', 'count', 'show', 'destroy', 'update'] + + types = { + 'number_rooms': int, 'number_bathrooms': int, + 'max_guest': int, 'price_by_night': int, + 'latitude': float, 'longitude': float + } def preloop(self): - """Print if isatty is false.""" + """Prints if isatty is false""" if not sys.__stdin__.isatty(): print('(hbnb)') @@ -41,122 +44,115 @@ def precmd(self, line): Usage: .([ [<*args> or <**kwargs>]]) (Brackets denote optional fields in usage example.) """ - # substitute single quote for double to handle json parsing - line = re.sub(r'[\']', '"', line[:]) - _cmd = _cls = _id = _args = '' # initialize line elements # scan for general formating - i.e '.', '(', ')' - if not all(char in line for char in {'.', '(', ')'}): - # if not ('.' in line and '(' in line and ')' in line): + if not ('.' in line and '(' in line and ')' in line): return line try: # parse line left to right - parsed_line = line[:] # parsed line + pline = line[:] # parsed line # isolate - _cls = parsed_line[:parsed_line.find('.')] + _cls = pline[:pline.find('.')] # isolate and validate - _cmd = parsed_line[parsed_line.find('.') + 1:parsed_line.find('(')] - + _cmd = pline[pline.find('.') + 1:pline.find('(')] if _cmd not in HBNBCommand.dot_cmds: raise Exception # if parantheses contain arguments, parse them - args = parsed_line[parsed_line.find('(') + 1:parsed_line.find(')')] - if args: + pline = pline[pline.find('(') + 1:pline.find(')')] + if pline: # partition args: (, [], [<*args>]) - _id, _, attr_name_and_value = args.partition(', ') -# pline = pline.partition(', ') # pline convert to tuple + pline = pline.partition(', ') # pline convert to tuple - # strip leading quotes and spaces - _id = _id.strip('\'" ') + # isolate _id, stripping quotes + _id = pline[0].strip('\'"') # possible bug here: # empty quotes register as empty _id when replaced - # strip leading quotes and spaces - attr_name_and_value = attr_name_and_value.strip('\'" ') - # if arguments exist beyond _id - if attr_name_and_value: + pline = pline[2].strip() # pline is now str + if pline: # check for *args or **kwargs - if attr_name_and_value.startswith("{") or\ - attr_name_and_value.endswith("}"): - _args = attr_name_and_value + if pline[0] == '{' and pline[-1] == '}'\ + and type(eval(pline)) is dict: + _args = pline else: - _args = attr_name_and_value.replace(',', '') - + _args = pline.replace(',', '') + # _args = _args.replace('\"', '') line = ' '.join([_cmd, _cls, _id, _args]) + except Exception as mess: pass - finally: return line def postcmd(self, stop, line): - """Prints if isatty is false.""" - if not sys.__stdin__.isatty() and line.strip() != "": + """Prints if isatty is false""" + if not sys.__stdin__.isatty() and line.strip() != "" : # 'and line.strip() != ""' added to Suppress Extra (hbnb) Prompt in postcmd print('(hbnb) ', end='') return stop - def do_quit(self, line): - """Method to exit the HBNB console.""" + def do_quit(self, command): + """ Method to exit the HBNB console""" return True def help_quit(self): - """Prints the help documentation for quit.""" + """ Prints the help documentation for quit """ print("Exits the program with formatting\n") - def do_EOF(self, line): - """Handles EOF to exit program.""" + def do_EOF(self, arg): + """ Handles EOF to exit program """ return True def help_EOF(self): - """Prints the help documentation for EOF.""" + """ Prints the help documentation for EOF """ print("Exits the program without formatting\n") def emptyline(self): - """Overrides the emptyline method of CMD.""" + """ Overrides the emptyline method of CMD """ pass def do_create(self, args): - """Create an object of any class.""" + """ Create an object of any class""" cls, _, params = args.partition(" ") - + if not cls: print("** class name missing **") return - + if cls not in HBNBCommand.classes: print("** class doesn't exist **") return - new_instance = HBNBCommand.classes[cls]() - if params: + # substitute single quote for double to handle json parsing + params = re.sub(r'[\']', '"', params) + + # patterns for the attributes' values in the param syntax = strings = r"""["'](?P[A-Za-z0-9_,!@#$%^&*\.-]+)["']""" floats = r"(?P(?:[-]?[0-9]+(?=\.))(?:\.[0-9]*(?![\.0-9-]+))" ints = r"(?P[-]?[0-9]+(?=\s))" - - # pattern for param syntax: - # = [=...] + + # pattern for param syntax: = [=...] attr_pattern = rf""" \b(?P[A-Za-z_]+)(?=\=) # Attribute name (?:\=) - (?P{strings}|(?P{ints}|{floats}))) # Attr value + (?P{strings}|(?P{ints}|{floats}))) # Attribute value (?:\s*) """ + pattern = re.compile(attr_pattern, re.VERBOSE) matches = pattern.findall(params) - + if matches: attr_dict = {} for match in matches: key, value, str_val, num_val, _, _ = match try: - val = json.loads( - value) if str_val else json.loads(num_val) + val = json.loads(value) if str_val else json.loads(num_val) except (SyntaxError, json.JSONDecodeError) as e: pass @@ -169,95 +165,96 @@ def do_create(self, args): attr_dict[key] = val new_instance = HBNBCommand.classes[cls](**attr_dict) - - try: - new_instance.save() - except sqlalchemy.exc.IntegrityError: - storage.rollback() - return + new_instance.save() + print(new_instance.id) + else: + print("Invalid param syntax. ") + print("[Usage]: create = [=...]") + else: + new_instance = HBNBCommand.classes[cls]() + new_instance.save() print(new_instance.id) def help_create(self): - """Help information for the create method.""" + """ Help information for the create method """ print("Creates a class of any type") - print("[Usage]: create \n") - print("[Usage]: create =" + - " [=...]\n") + print("[Usage]: create [=...]\n") - def do_show(self, line): - """Method to show an individual object.""" - cls, _, instance_id = line.partition(" ") + def do_show(self, args): + """ Method to show an individual object """ + new = args.partition(" ") + c_name = new[0] + c_id = new[2] - if not cls: + # guard against trailing args + if c_id and ' ' in c_id: + c_id = c_id.partition(' ')[0] + + if not c_name: print("** class name missing **") return - if cls not in HBNBCommand.classes: + if c_name not in HBNBCommand.classes: print("** class doesn't exist **") return - if not instance_id: + if not c_id: print("** instance id missing **") return - instance_id = instance_id.strip('\'" ') - - key = f"{cls}.{instance_id}" - - if key not in storage.all().keys(): + key = c_name + "." + c_id + try: + print(storage.all()[key]) + except KeyError: print("** no instance found **") - return - - print(storage.all()[key]) def help_show(self): - """Help information for the show command.""" + """ Help information for the show command """ print("Shows an individual instance of a class") print("[Usage]: show \n") - def do_destroy(self, line): - """Destroy a specified object.""" - cls, _, instance_id = line.partition(" ") + def do_destroy(self, args): + """ Destroys a specified object """ + new = args.partition(" ") + c_name = new[0] + c_id = new[2] + if c_id and ' ' in c_id: + c_id = c_id.partition(' ')[0] - if not cls: + if not c_name: print("** class name missing **") return - if cls not in HBNBCommand.classes: + if c_name not in HBNBCommand.classes: print("** class doesn't exist **") return - if not instance_id: + if not c_id: print("** instance id missing **") return - instance_id = instance_id.strip('\'" ') - - key = f"{cls}.{instance_id}" + key = c_name + "." + c_id - if key not in storage.all().keys(): + try: + storage.delete(storage.all()[key]) + except KeyError: print("** no instance found **") - return - - storage.delete(storage.all().copy()[key]) def help_destroy(self): - """Help information for the destroy command.""" + """ Help information for the destroy command """ print("Destroys an individual instance of a class") print("[Usage]: destroy \n") def do_all(self, args): - """Show all objects, or all objects of a class.""" + """ Shows all objects, or all objects of a class""" print_list = [] if args: cls = args.split(' ')[0] # remove possible trailing args - if cls not in HBNBCommand.classes: print("** class doesn't exist **") return - for key, value in storage.all().items(): if key.split('.')[0] == cls: print_list.append(str(value)) @@ -268,14 +265,13 @@ def do_all(self, args): print(print_list) def help_all(self): - """Help information for the all command.""" + """ Help information for the all command """ print("Shows all objects, or all of a class") print("[Usage]: all \n") - def do_count(self, line): - """Count current number of class instances.""" - - cls, _, _ = line.partition(" ") + def do_count(self, args): + """Count current number of class instances""" + cls, _, _ = args.partition(" ") if not cls: print("** class name missing **") @@ -284,106 +280,104 @@ def do_count(self, line): if cls not in HBNBCommand.classes: print("** class doesn't exist **") return - + count = 0 - for key in storage.all(): - if cls == key.split('.')[0]: + for k, v in storage.all().items(): + if cls == k.split('.')[0]: count += 1 - + print(count) def help_count(self): - """Help information for the count command.""" + """ """ print("Usage: count ") - def do_update(self, line): - """Update a certain object with new info.""" - # isolate cls from id/args - cls, _, args = line.strip().partition(" ") + def do_update(self, args): + """ Updates a certain object with new info """ + + c_name = c_id = att_name = att_val = kwargs = '' - if not cls: + # isolate cls from id/args, ex: (, delim, ) + args = args.partition(" ") + if args[0]: + c_name = args[0] + else: # class name not present print("** class name missing **") return - - if cls not in HBNBCommand.classes: # class name invalid + if c_name not in HBNBCommand.classes: # class name invalid print("** class doesn't exist **") return # isolate id from args - instance_id, _, args_or_kwargs = args.strip().partition(" ") - instance_id = instance_id.strip("\"' ") - args_or_kwargs = args_or_kwargs.strip("\"' ") - - if not instance_id: + args = args[2].partition(" ") + if args[0]: + c_id = args[0] + else: # id not present print("** instance id missing **") return # generate key from class and id - key = f"{cls}.{instance_id}" + key = c_name + "." + c_id # determine if key is present if key not in storage.all(): print("** no instance found **") return - # Retrieve object to update its attributes - updated_obj = storage.all()[key] - - if args_or_kwargs: - if any(char in args_or_kwargs for char in '{}'): - # Handle dictionary representation - try: - # Safely evaluates Python dictionary-like strings - attr_dict = ast.literal_eval(args_or_kwargs.strip("\"' ")) - if not isinstance(attr_dict, dict): - raise ValueError - except (SyntaxError, ValueError): - print("** invalid dictionary representation **") + # first determine if kwargs or args + if '{' in args[2] and '}' in args[2] and type(eval(args[2])) is dict: + kwargs = eval(args[2]) + args = [] # reformat kwargs into list, ex: [, , ...] + for k, v in kwargs.items(): + args.append(k) + args.append(v) + else: # isolate args + args = args[2] + if args and args[0] == '\"': # check for quoted arg + second_quote = args.find('\"', 1) + att_name = args[1:second_quote] + args = args[second_quote + 1:] + + args = args.partition(' ') + + # if att_name was not quoted arg + if not att_name and (not args[0].isspace()): + att_name = args[0] + # check for quoted val arg + if args[2] and args[2][0] == '\"': + att_val = args[2][1:args[2].find('\"', 1)] + + # if att_val was not quoted arg + if not att_val and args[2]: + att_val = args[2].partition(' ')[0] + + args = [att_name, att_val] + + # retrieve dictionary of current objects + new_dict = storage.all()[key] + + # iterate through attr names and values + for i, att_name in enumerate(args): + # block only runs on even iterations + if (i % 2 == 0): + att_val = args[i + 1] # following item is value + if not att_name: # check for att_name + print("** attribute name missing **") return - else: - for attr_name, attr_value in attr_dict.items(): - attr_name = attr_name.strip("\"' ") - - if isinstance(attr_value, str): - attr_value = attr_value.strip("\"' ") - - setattr(updated_obj, attr_name, attr_value) - - else: - attr_name, _, attr_value = args_or_kwargs.partition(" ") - - if not attr_value: + if not att_val: # check for att_value print("** value missing **") return + # type cast as necessary + if att_name in HBNBCommand.types: + att_val = HBNBCommand.types[att_name](att_val) - attr_name = attr_name.strip("\"' ") - - attr_value = attr_value.strip("\"' ") - - # guard against trailing args - attr_value, _, xtra_args = attr_value.partition(" ") - - attr_value = attr_value.strip("\"' ") - - if not xtra_args: - try: - # Convert to appropriate type if possible - attr_value = json.loads(attr_value) - except (json.JSONDecodeError): - pass # Keep as string if evaluation fails - except Exception as e: - pass - finally: - setattr(updated_obj, attr_name, attr_value) - - else: - print("** attribute name missing **") - return + # update dictionary with name, value pair + new_dict.__dict__.update({att_name: att_val}) - updated_obj.save() # Save the updated object + new_dict.save() # save updates to file def help_update(self): - """Help information for the update class.""" + """ Help information for the update class """ print("Updates an object with new information") print("Usage: update \n") From 1fe3e4ae40c69ea316125c17a2f76b54947bc4ad Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 21:02:04 +0300 Subject: [PATCH 093/101] style: pycodestyle formatting --- console.py | 55 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/console.py b/console.py index 1fdefe78c537..7d314ba898ad 100755 --- a/console.py +++ b/console.py @@ -21,17 +21,17 @@ class HBNBCommand(cmd.Cmd): prompt = '(hbnb) ' if sys.__stdin__.isatty() else '' classes = { - 'BaseModel': BaseModel, 'User': User, 'Place': Place, - 'State': State, 'City': City, 'Amenity': Amenity, - 'Review': Review - } + 'BaseModel': BaseModel, 'User': User, 'Place': Place, + 'State': State, 'City': City, 'Amenity': Amenity, + 'Review': Review + } dot_cmds = ['all', 'count', 'show', 'destroy', 'update'] - + types = { - 'number_rooms': int, 'number_bathrooms': int, - 'max_guest': int, 'price_by_night': int, - 'latitude': float, 'longitude': float - } + 'number_rooms': int, 'number_bathrooms': int, + 'max_guest': int, 'price_by_night': int, + 'latitude': float, 'longitude': float + } def preloop(self): """Prints if isatty is false""" @@ -91,7 +91,9 @@ def precmd(self, line): def postcmd(self, stop, line): """Prints if isatty is false""" - if not sys.__stdin__.isatty() and line.strip() != "" : # 'and line.strip() != ""' added to Suppress Extra (hbnb) Prompt in postcmd + if not sys.__stdin__.isatty() and line.strip() != "": + # 'and line.strip() != ""' added to Suppress Extra + # (hbnb) Prompt in postcmd print('(hbnb) ', end='') return stop @@ -118,11 +120,11 @@ def emptyline(self): def do_create(self, args): """ Create an object of any class""" cls, _, params = args.partition(" ") - + if not cls: print("** class name missing **") return - + if cls not in HBNBCommand.classes: print("** class doesn't exist **") return @@ -130,29 +132,32 @@ def do_create(self, args): if params: # substitute single quote for double to handle json parsing params = re.sub(r'[\']', '"', params) - - # patterns for the attributes' values in the param syntax = + + # patterns for the attributes' values in the param syntax + # = strings = r"""["'](?P[A-Za-z0-9_,!@#$%^&*\.-]+)["']""" floats = r"(?P(?:[-]?[0-9]+(?=\.))(?:\.[0-9]*(?![\.0-9-]+))" ints = r"(?P[-]?[0-9]+(?=\s))" - - # pattern for param syntax: = [=...] + + # pattern for param syntax: + # = [=...] attr_pattern = rf""" \b(?P[A-Za-z_]+)(?=\=) # Attribute name (?:\=) - (?P{strings}|(?P{ints}|{floats}))) # Attribute value + (?P{strings}|(?P{ints}|{floats}))) # Attr value (?:\s*) """ pattern = re.compile(attr_pattern, re.VERBOSE) matches = pattern.findall(params) - + if matches: attr_dict = {} for match in matches: key, value, str_val, num_val, _, _ = match try: - val = json.loads(value) if str_val else json.loads(num_val) + val = json.loads( + value) if str_val else json.loads(num_val) except (SyntaxError, json.JSONDecodeError) as e: pass @@ -169,8 +174,10 @@ def do_create(self, args): print(new_instance.id) else: print("Invalid param syntax. ") - print("[Usage]: create = [=...]") - + print( + "[Usage]: create " + + "= [=...]") + else: new_instance = HBNBCommand.classes[cls]() new_instance.save() @@ -280,12 +287,12 @@ def do_count(self, args): if cls not in HBNBCommand.classes: print("** class doesn't exist **") return - + count = 0 for k, v in storage.all().items(): if cls == k.split('.')[0]: count += 1 - + print(count) def help_count(self): @@ -294,7 +301,7 @@ def help_count(self): def do_update(self, args): """ Updates a certain object with new info """ - + c_name = c_id = att_name = att_val = kwargs = '' # isolate cls from id/args, ex: (, delim, ) From 3cfaf366c600a2c65ec6099cbd94b6d1ed23ad2b Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 21:52:35 +0300 Subject: [PATCH 094/101] refactor: do_all and do_count to reload storage explicitly --- console.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/console.py b/console.py index 7d314ba898ad..64dd69579c0d 100755 --- a/console.py +++ b/console.py @@ -257,6 +257,8 @@ def do_all(self, args): """ Shows all objects, or all objects of a class""" print_list = [] + storage.reload() + if args: cls = args.split(' ')[0] # remove possible trailing args if cls not in HBNBCommand.classes: @@ -288,6 +290,8 @@ def do_count(self, args): print("** class doesn't exist **") return + storage.reload() + count = 0 for k, v in storage.all().items(): if cls == k.split('.')[0]: From 9edef16f601755e1ef78fa2560faa54f13e3a861 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 21:54:28 +0300 Subject: [PATCH 095/101] refactor: refactor all method to not reload storage, reload method from catching exceptions --- models/engine/db_storage.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/models/engine/db_storage.py b/models/engine/db_storage.py index aba76f8c80aa..94d3cf712c1e 100755 --- a/models/engine/db_storage.py +++ b/models/engine/db_storage.py @@ -102,7 +102,6 @@ def all(self, cls=None): `.` and the value is the object itself. """ - self.reload() if cls is not None: classes = ('BaseModel', @@ -162,26 +161,20 @@ def reload(self): This method creates all tables if they don't exist and initializes a new session. """ - try: - # Bind the engine to the Base's metadata - Base.metadata.bind = self.__engine + # Bind the engine to the Base's metadata + Base.metadata.bind = self.__engine - # Create all tables defined in the Base - Base.metadata.create_all(self.__engine) + # Create all tables defined in the Base + Base.metadata.create_all(self.__engine) - # Create a session factory - session_factory = sessionmaker(bind=self.__engine, - expire_on_commit=False) + # Create a session factory + session_factory = sessionmaker(bind=self.__engine, + expire_on_commit=False) - # Create a scoped session - Session = scoped_session(session_factory) + # Create a scoped session + Session = scoped_session(session_factory) - except OperationalError as e: - message = getattr(e, '_message') - print(message().partition(" ") - [-1].strip('()').split(',')[-1]) - else: - self.__session = Session() + self.__session = Session() def delete(self, obj=None): """ From 00783b2d70bcf655890f2b000685810ca13d1394 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 22:05:25 +0300 Subject: [PATCH 096/101] refactor --- models/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/__init__.py b/models/__init__.py index 102748b5b104..ddadb75fa491 100755 --- a/models/__init__.py +++ b/models/__init__.py @@ -8,9 +8,9 @@ if os.getenv("HBNB_TYPE_STORAGE") == "db": from models.engine.db_storage import DBStorage storage = DBStorage() + storage.reload() else: from models.engine.file_storage import FileStorage storage = FileStorage() - -storage.reload() + storage.reload() From aea59f1d193c076ec7451b2ae4c88787b25126f6 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 22:24:44 +0300 Subject: [PATCH 097/101] refactor: refactor else statements --- console.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/console.py b/console.py index 64dd69579c0d..eb3860305bf0 100755 --- a/console.py +++ b/console.py @@ -129,6 +129,8 @@ def do_create(self, args): print("** class doesn't exist **") return + new_instance = HBNBCommand.classes[cls]() + if params: # substitute single quote for double to handle json parsing params = re.sub(r'[\']', '"', params) @@ -170,18 +172,21 @@ def do_create(self, args): attr_dict[key] = val new_instance = HBNBCommand.classes[cls](**attr_dict) - new_instance.save() - print(new_instance.id) - else: - print("Invalid param syntax. ") - print( - "[Usage]: create " + - "= [=...]") - - else: - new_instance = HBNBCommand.classes[cls]() - new_instance.save() - print(new_instance.id) + # new_instance.save() + # print(new_instance.id) + # else: + # print("Invalid param syntax. ") + # print( + # "[Usage]: create " + + # "= [=...]") + + # else: + # new_instance = HBNBCommand.classes[cls]() + # new_instance.save() + # print(new_instance.id) + + new_instance.save() + print(new_instance.id) def help_create(self): """ Help information for the create method """ From 53056d1cea4aa985ab30d1246a567ddb64e2130b Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 22:29:33 +0300 Subject: [PATCH 098/101] refactor: try/else/finally statement added --- console.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/console.py b/console.py index eb3860305bf0..fb915e52d58d 100755 --- a/console.py +++ b/console.py @@ -4,6 +4,7 @@ import sys import re import json +import sqlalchemy from models.base_model import BaseModel from models.user import User from models.place import Place @@ -184,9 +185,13 @@ def do_create(self, args): # new_instance = HBNBCommand.classes[cls]() # new_instance.save() # print(new_instance.id) - - new_instance.save() - print(new_instance.id) + try: + new_instance.save() + except sqlalchemy.exc.IntegrityError: + storage.rollback() + return + else: + print(new_instance.id) def help_create(self): """ Help information for the create method """ From 8b480401560b357c3634fa01f103afd8fb14b342 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 22:32:31 +0300 Subject: [PATCH 099/101] style: removed commented out statemeents --- console.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/console.py b/console.py index fb915e52d58d..3db8a5ca8baf 100755 --- a/console.py +++ b/console.py @@ -173,18 +173,7 @@ def do_create(self, args): attr_dict[key] = val new_instance = HBNBCommand.classes[cls](**attr_dict) - # new_instance.save() - # print(new_instance.id) - # else: - # print("Invalid param syntax. ") - # print( - # "[Usage]: create " + - # "= [=...]") - - # else: - # new_instance = HBNBCommand.classes[cls]() - # new_instance.save() - # print(new_instance.id) + try: new_instance.save() except sqlalchemy.exc.IntegrityError: From df5849a4294b463cc2bbb1e1543ea741b5e7cdf7 Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 23:25:21 +0300 Subject: [PATCH 100/101] update --- console.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/console.py b/console.py index 3db8a5ca8baf..1739f89f7639 100755 --- a/console.py +++ b/console.py @@ -153,7 +153,6 @@ def do_create(self, args): pattern = re.compile(attr_pattern, re.VERBOSE) matches = pattern.findall(params) - if matches: attr_dict = {} for match in matches: @@ -171,12 +170,11 @@ def do_create(self, args): attr_dict[key] = val.translate(translator) else: attr_dict[key] = val - new_instance = HBNBCommand.classes[cls](**attr_dict) try: new_instance.save() - except sqlalchemy.exc.IntegrityError: + except sqlalchemy.exc.IntegrityError as e: storage.rollback() return else: From 43621f9c02786b8af3935070c1fd82890eed1d1a Mon Sep 17 00:00:00 2001 From: mwanzaalbert Date: Sat, 15 Feb 2025 23:41:57 +0300 Subject: [PATCH 101/101] fix: removed default values from latitude and longitude columns --- models/place.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/models/place.py b/models/place.py index 9b93e52200cb..f3de499a076c 100755 --- a/models/place.py +++ b/models/place.py @@ -10,8 +10,6 @@ """ import os from models.base_model import BaseModel, Base -# from models.amenity import Amenity -# from models.review import Review from sqlalchemy.orm import relationship from sqlalchemy import Integer, Column, String, ForeignKey, Float, Table from typing import List @@ -59,8 +57,6 @@ class Place(BaseModel, Base): __tablename__ = "places" - amenity_ids: List[str] = [] - city_id = Column(String(60), ForeignKey('cities.id'), nullable=False) user_id = Column(String(60), ForeignKey('users.id'), nullable=False) name = Column(String(128), nullable=False) @@ -69,8 +65,8 @@ class Place(BaseModel, Base): number_bathrooms = Column(Integer, nullable=False, default=0) max_guest = Column(Integer, nullable=False, default=0) price_by_night = Column(Integer, nullable=False, default=0) - latitude = Column(Float, nullable=True, default=0.0) - longitude = Column(Float, nullable=True, default=0.0) + latitude = Column(Float, nullable=True) + longitude = Column(Float, nullable=True) if os.getenv("HBNB_TYPE_STORAGE") == 'db': reviews = relationship('Review', @@ -93,6 +89,7 @@ class Place(BaseModel, Base): price_by_night: int = 0 latitude: float = 0.0 longitude: float = 0.0 + amenity_ids: List[str] = [] @property def reviews(self):