Repository: StenTech/FolderForge (language: python)

Issue:
Feat/cli v1.0.1
- [x]: manage cli in __main__.py


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 2107bcb..10747a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,9 @@
 __pycache__
-/src/folderforge/test*
-/src/folderforge.egg*
-dist
\ No newline at end of file
+build
+folderforge.egg*
+.vscode
+env
+ignore_this_test.py
+examples
+project
+core.py
\ No newline at end of file
diff --git a/README.md b/README.md
index abf3545..26ab94f 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
 # FolderForge
 A simple and user-friendly tool for generating directory and file structures based on a defined format in JSON, YAML, or other formats.
+
diff --git a/description.json b/description.json
new file mode 100644
index 0000000..ef35d76
--- /dev/null
+++ b/description.json
@@ -0,0 +1,83 @@
+{
+	"path": "project/sub1/sub2/",
+	"tree": [
+		{
+			"name": "examples",
+			"type": "directory",
+			"children": [
+				{
+					"name": "project_structure.json",
+					"type": "file"
+				},
+				{
+					"name": "website_structure.yaml",
+					"type": "file"
+				}
+			]
+		},
+		{
+			"name": "folderforge",
+			"type": "directory",
+			"children": [
+				{
+					"name": "init.py",
+					"type": "file",
+					"content": {
+						"type": "plaintext",
+						"value": "Hello World"
+					}
+				},
+				{
+					"name": "core.py",
+					"type": "file",
+					"content": {
+						"type": "filepath",
+						"value": ".gitignore"
+					}
+				},
+				{
+					"name": "file_formats.py",
+					"type": "file",
+					"content": {
+						"type": "url",
+						"value": "https://jsonip.com"
+					}
+				}
+			]
+		},
+		{
+			"name": "tests",
+			"type": "directory",
+			"children": [
+				{
+					"name": "test_core.py",
+					"type": "file"
+				},
+				{
+					"name": "test_file_formats.py",
+					"type": "file"
+				}
+			]
+		},
+		{
+			"name": ".ggitignore",
+			"type": "file"
+		},
+		{
+			"name": "folderforge_cli.py",
+			"type": "file"
+		},
+		{
+			"name": "LLICENSE",
+			"type": "file"
+		},
+		{
+			"name": "RREADME.md",
+			"type": "file"
+		},
+		{
+			"name": "prequirements.txt",
+			"type": "file"
+		}
+	]
+}
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index a9a411e..1c20464 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,7 +20,11 @@ package_dir =
 packages = find:
 python_requires = >=3.9
 install_requires =
-
+	requests==2.28.2
 
 [options.packages.find]
 where = src
+
+[options.entry_points]
+console_scripts =
+    folderforge = folderforge.__main__:main
diff --git a/src/folderforge/__init__.py b/src/folderforge/__init__.py
index aa6cbf7..5ff280b 100644
--- a/src/folderforge/__init__.py
+++ b/src/folderforge/__init__.py
@@ -1,7 +1,63 @@
+from os.path import dirname, join
+
+try:
+	from services import (
+		FolderForgeService,
+		FileService,
+		ChangeDirectoryContext
+	)
+	from utils.styles import Color, Style
+	from models import File
+except:
+	from .services import (
+		FolderForgeService,
+		FileService,
+		ChangeDirectoryContext
+	)
+	from .utils.styles import Color, Style
+	from .models import File
+
 
 class FolderForge:
-	def __init__(self, config_file_path: str):
-		self.config_file_path = config_file_path
-	
-	def forge(self):
-		pass
\ No newline at end of file
+	def __init__(self, description_file_path: str):
+		self.description_file_path = description_file_path
+		description = FolderForgeService.readJSON(description_file_path)
+		self.path = description.get("path", "")
+		self.tree = description.get("tree", [])
+		
+	def forge(self, path: str=""):
+		"""
+		Create the folder structure
+		path: the path to the root directory in which the folder structure will be created, 
+		it can be empty, a simple name or a path
+		"""
+
+		if path=="":
+			path = self.path
+			
+		FileService.createDirectoryWithSubDirs(path)
+
+		with ChangeDirectoryContext(path):	
+			for node in FolderForgeService.searchPaths(self.tree):
+				type = node["type"]
+				path = node["path"]
+				content = node.get("content")
+				
+				print(Color.primary("[FOLDERFORGE]: Creating " + Style.bold(type)) + Color.primary(" " + path))
+
+				if type == "file":
+					file: File = FileService.createFile(path)
+
+					# manage file content
+					if content:
+						file.putContent(
+							content_type=content["type"],
+							content_value=content["value"],
+							description_file_path=self.description_file_path
+						)
+				elif type == "directory":
+					FileService.createDirectory(path)
+				else:
+					raise Exception("Unknown node type: " + type)
+			
+			return self
diff --git a/src/folderforge/__main__.py b/src/folderforge/__main__.py
index 77529b2..bf7f16a 100755
--- a/src/folderforge/__main__.py
+++ b/src/folderforge/__main__.py
@@ -1,2 +1,37 @@
 #! /usr/bin/env python3
+from os import path, system
 
+try:
+	from __init__ import FolderForge
+	from utils.styles import Color
+	from utils.functions import parser
+except:
+	from . import FolderForge
+	from .utils.styles import Color
+	from .utils.functions import parser
+
+
+# the main function for all services testing
+def main():
+	args = parser()
+	description_file = path.abspath(args.description_file)
+
+	folderForge = FolderForge(description_file)
+	project_path = path.abspath(args.path if len(args.path) > 0 else folderForge.path)
+	
+	# clear console
+	system("cls | clear")
+	
+	# Printing the description file path
+	print("[FOLDERFORGE]: Description file path: " + Color.primary(description_file))
+	# Printing the path to the root directory in which the folder structure will be created
+	print("[FOLDERFORGE]: Root directory path: " + Color.primary(project_path))
+	print(Color.warning("\n[FOLDERFORGE]: Creating the folder structure...\n"))
+	
+	folderForge.forge(project_path)
+
+	# With success emoji and party popper emoji \U0001F389 \U0001F389  
+	print(Color.success("\n[FOLDERFORGE]:  \U0001F600 Folder structure created successfully!!!\U0001F389 \U0001F389 \U0001F389 \n"))
+
+if __name__ == "__main__":
+	main()
\ No newline at end of file
diff --git a/src/folderforge/models/__init__.py b/src/folderforge/models/__init__.py
new file mode 100644
index 0000000..87fe431
--- /dev/null
+++ b/src/folderforge/models/__init__.py
@@ -0,0 +1,4 @@
+try:
+	from file import File
+except:
+	from .file import File
diff --git a/src/folderforge/models/file.py b/src/folderforge/models/file.py
new file mode 100644
index 0000000..21cc8f4
--- /dev/null
+++ b/src/folderforge/models/file.py
@@ -0,0 +1,101 @@
+import requests
+from os.path import dirname, join
+
+try:
+	from utils.functions import getFileContent
+	from utils.enums import CONTENT_TYPES
+	from utils.styles import Style
+except:
+	from ..utils.functions import getFileContent
+	from ..utils.enums import CONTENT_TYPES
+	from ..utils.styles import Style
+
+
+class File:
+	def __init__(self, path):
+		"""
+		Initialize a File object with the specified path.
+
+		Args:
+			path (str): The path to the file.
+		"""
+
+		self.path = path
+
+	def putContent(self, content_type: str, content_value: str, description_file_path: str):
+		"""
+		Put content into the file based on the specified content type.
+
+		Args:
+			content_type (str): The type of content to be added. Should be one of CONTENT_TYPES enum values.
+			content_value (str): The actual content to be added.
+			description_file_path (str): The path to the description file.
+
+		Raises:
+			ValueError: If an unsupported content type is provided.
+		"""
+
+		content_type = content_type.upper()
+
+		# Use a match statement to handle different content types
+		match content_type:
+			case CONTENT_TYPES.PLAINTEXT:
+				self._putPlaintextContent(content_value)
+			case CONTENT_TYPES.FILEPATH:
+				self._putFilePathContent(
+					filepath=content_value, 
+					parent_dir=dirname(description_file_path)
+				)
+			case CONTENT_TYPES.URL:
+				self._putUrlContent(content_value)
+			case _:
+				pass
+	
+	def _putPlaintextContent(self, plaintext: str):
+		"""
+        Put plaintext content into the file.
+
+        Args:
+            plaintext (str): The plaintext content to be added to the file.
+        """
+
+		with open(self.path, "w") as file:
+			file.write(plaintext)
+	
+	def _putFilePathContent(self, filepath: str, parent_dir: str):
+		"""
+		Put content from another file into the current file.
+
+		Args:
+			filepath (str): The path to the file whose content should be added.
+			parent_dir (str): The directory containing the description file.
+		"""
+
+		abs_filepath = join(parent_dir, filepath)
+
+		try:
+			content = getFileContent(abs_filepath)
+		except:
+			raise ValueError(Style.bold(f"file {abs_filepath} not exists or it's not plaintext file"))
+
+		# put content to the file
+		with open(self.path, "w") as file:
+			file.write(content)
+
+	def _putUrlContent(self, url: str):
+		"""
+		Download content from a URL and put it into the file.
+
+		Args:
+			url (str): The URL from which to download content.
+		"""
+
+		# make the request to retrieve the text response from the url
+		response = requests.get(url)
+
+		if response.status_code == 200:
+			with open(self.path, "w") as file:
+				file.write(response.text)
+		else:
+			raise ValueError(f"Retrieving data from the URL {url} for the file {self.path} failed. Verify that the URL is correct and accessible.")
+		
\ No newline at end of file
diff --git a/src/folderforge/services/__init__.py b/src/folderforge/services/__init__.py
new file mode 100644
index 0000000..52550b3
--- /dev/null
+++ b/src/folderforge/services/__init__.py
@@ -0,0 +1,8 @@
+try:
+	from file_service import FileService
+	from folderforge_service import FolderForgeService
+	from change_directory_context import ChangeDirectoryContext
+except:
+	from .file_service import FileService
+	from .folderforge_service import FolderForgeService
+	from .change_directory_context import ChangeDirectoryContext
\ No newline at end of file
diff --git a/src/folderforge/services/change_directory_context.py b/src/folderforge/services/change_directory_context.py
new file mode 100644
index 0000000..51f9640
--- /dev/null
+++ b/src/folderforge/services/change_directory_context.py
@@ -0,0 +1,13 @@
+import os
+
+class ChangeDirectoryContext:
+	def __init__(self, new_directory):
+		self.new_directory = new_directory
+		self.old_directory = os.getcwd()
+
+	def __enter__(self):
+		if self.new_directory != "":
+			os.chdir(self.new_directory)
+
+	def __exit__(self, *args):
+			os.chdir(self.old_directory)
\ No newline at end of file
diff --git a/src/folderforge/services/file_service.py b/src/folderforge/services/file_service.py
new file mode 100644
index 0000000..670c170
--- /dev/null
+++ b/src/folderforge/services/file_service.py
@@ -0,0 +1,65 @@
+import os
+
+try:
+	from models import File
+except:
+	from ..models import File
+
+
+class FileService:
+
+	@classmethod
+	def createFile(cls, path: str) -> File:
+		"""
+		Create a new file
+		"""
+
+		with open(path, "w"):
+			pass
+		
+		# return File instance
+		file = File(path)
+
+		return file
+
+	@classmethod
+	def createDirectory(cls, path: str):
+		"""
+		Create a new directory
+		"""
+
+		if not cls.pathExists(path):
+			os.mkdir(path)
+
+	@classmethod
+	def createDirectoryWithSubDirs(cls, path: str):
+		"""
+		Create a new directory with subdirectories
+		"""
+		if not cls.pathExists(path):
+			os.makedirs(path, exist_ok=True)
+
+		
+
+	@classmethod
+	def pathExists(cls, path: str):
+		"""
+		Check if a path exists
+		"""
+
+		return os.path.exists(path)
+	
+	@classmethod
+	def isdIr(cls, path: str) -> bool:
+		"""
+		Check if a path is a directory
+		"""
+
+		return os.path.isdir(path)
+
+
+def main():
+	FileService.createDirectoryWithSubDirs("home/stentech/Documents/project/folderforge/src/folderforge")
+
+if __name__ == "__main__":
+	main()
\ No newline at end of file
diff --git a/src/folderforge/services/folderforge_service.py b/src/folderforge/services/folderforge_service.py
new file mode 100644
index 0000000..7d878ac
--- /dev/null
+++ b/src/folderforge/services/folderforge_service.py
@@ -0,0 +1,47 @@
+import os
+import json
+
+
+class FolderForgeService:
+	
+	@classmethod
+	def readJSON(cls, path: str):
+		"""
+		Read a json file and return its content as a dictionary
+		"""
+		return json.load(open(path))
+	
+	@classmethod
+	def searchPaths(cls, tree: list):
+		"""
+		An iterator method/function to
+		explore tree nodes using DFS algorithm and create path property for all the node
+
+		Example:
+			for node in FolderForgeService.searchPaths(tree):
+				print(node["path"])
+		"""
+
+		cur_list = tree
+		visited = []
+
+		while cur_list:
+			node = cur_list.pop()
+			visited.append(node) 
+
+			if not node.get("path"):
+				node["path"] = node["name"]  
+
+			# return node for the current iteration
+			yield node
+
+			# explore node children if exists
+			for child in node.get("children", []):
+				if child not in visited:
+					# child["parent"] = node["name"] + node.get("parent_path", "")
+					child["path"] = os.path.join(node.get("path", ""), child["name"])
+
+					cur_list.append(child)
+
+
+
diff --git a/src/folderforge/utils/Class.py b/src/folderforge/utils/Class.py
deleted file mode 100644
index 2d386d2..0000000
--- a/src/folderforge/utils/Class.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from os.path import isdir
-
-class Color:
-	success = lambda _str: '\033[92m' + _str + '\033[0m'
-	danger = lambda _str: '\033[91m' + _str + '\033[0m'
-	primary = lambda _str: '\033[96m' + _str + '\033[0m'
-	warning = lambda _str: '\033[33m' + _str + '\033[0m'
-
-class Style:
-	bold = lambda _str: '\033[1m' + _str + '\033[0m'
-	underline = lambda _str: '\033[4m' + _str + '\033[0m'
-	blink = lambda _str: '\033[5m' + _str + '\033[0m'
diff --git a/src/folderforge/utils/enums.py b/src/folderforge/utils/enums.py
new file mode 100644
index 0000000..79a680b
--- /dev/null
+++ b/src/folderforge/utils/enums.py
@@ -0,0 +1,6 @@
+
+
+class CONTENT_TYPES:
+	FILEPATH="FILEPATH" 
+	URL="URL"
+	PLAINTEXT="PLAINTEXT"
\ No newline at end of file
diff --git a/src/folderforge/utils/functions.py b/src/folderforge/utils/functions.py
new file mode 100644
index 0000000..b64de65
--- /dev/null
+++ b/src/folderforge/utils/functions.py
@@ -0,0 +1,51 @@
+import argparse
+
+
+def parser():
+	parser = argparse.ArgumentParser(
+		prog="folderforge",
+		description="A simple tool to create folder structures from a JSON file",
+		epilog="%(prog)s made with love by @StenTech and @RidoineEl, enjoy!!!",
+	)
+	parser.add_argument(
+    	"--version", action="version", version="%(prog)s 1.0.0"
+	)
+	parser.add_argument(
+		"description_file", 
+		help="The path to the structure file, it's a JSON file",
+		default="description.json",
+		nargs="?",
+		type=str
+	)
+	parser.add_argument(
+		"-p", 
+		"--path", 
+		help="The path to the root directory in which the folder structure will be created, it can be empty, a simple name or a path",
+		default="",
+		nargs="?", 
+		type=str
+	)
+	
+
+	return parser.parse_args()
+
+def getFileContent(path: str) -> str:
+	"""
+	return the file content as string
+	""" 
+
+	content = ""
+	
+	with open(path, "r") as file:
+		lines = file.readlines()
+		content = "".join(lines)
+
+	return content
+
+
+# def main():
+# 	args = parser()
+# 	print(args)	
+
+# if __name__ == "__main__":
+# 	main()
\ No newline at end of file
diff --git a/src/folderforge/utils/styles.py b/src/folderforge/utils/styles.py
new file mode 100644
index 0000000..85984b5
--- /dev/null
+++ b/src/folderforge/utils/styles.py
@@ -0,0 +1,12 @@
+from os.path import isdir
+
+class Color:
+	success = lambda _str: '\033[92m' + _str + '\033[0m' # green
+	danger = lambda _str: '\033[91m' + _str + '\033[0m' # red
+	primary = lambda _str: '\033[96m' + _str + '\033[0m' # cyan
+	warning = lambda _str: '\033[33m' + _str + '\033[0m' # yellow
+
+class Style:
+	bold = lambda _str: '\033[1m' + _str + '\033[0m'
+	underline = lambda _str: '\033[4m' + _str + '\033[0m'
+	blink = lambda _str: '\033[5m' + _str + '\033[0m'


Test patch:
diff --git a/src/tests/__init__.py b/src/tests/__init__.py
new file mode 100644
index 0000000..e69de29