swarm-debug 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
backend/__init__.py ADDED
File without changes
File without changes
File without changes
@@ -0,0 +1,83 @@
1
+ import logging
2
+ import json
3
+ import os
4
+ from backend.config.Apps import SubApp
5
+ from backend.core.models.project_scanner import update_debug_toggles, dir_to_output_format
6
+ from backend.core.data_dir import NEEDS_RESYNC_FILE, TOGGLE_FILE as DEBUG_TOGGLE_FILE
7
+ from backend.core.DEFAULTS import get_root_dir, set_root_dir
8
+ from contextlib import asynccontextmanager
9
+ from fastapi.responses import JSONResponse
10
+ from typeguard import typechecked
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ @asynccontextmanager
16
+ async def debugger_lifespan():
17
+ log.debug("debugger_lifespan START")
18
+ yield
19
+ log.debug("debugger_lifespan END")
20
+
21
+
22
+ debugger = SubApp("debugger", debugger_lifespan)
23
+
24
+
25
+ @debugger.router.get("/pull_structure")
26
+ @typechecked
27
+ async def pull_structure() -> JSONResponse:
28
+ log.info("GET /api/debugger/pull_structure")
29
+ scanned_dir = update_debug_toggles(save_to_file=True)
30
+ output = dir_to_output_format(scanned_dir)
31
+ return JSONResponse(content=output)
32
+
33
+
34
+ @debugger.router.post("/push_structure")
35
+ @typechecked
36
+ async def push_structure(data: dict) -> JSONResponse:
37
+ log.info("POST /api/debugger/push_structure")
38
+ project_structure = data['projectStructure']
39
+ with open(DEBUG_TOGGLE_FILE, 'w', encoding='utf-8') as file:
40
+ json.dump(project_structure, file, indent=4)
41
+ with open(NEEDS_RESYNC_FILE, 'w') as f:
42
+ f.write('1')
43
+ return JSONResponse(content={"status": "success"})
44
+
45
+
46
+ @debugger.router.post("/reset_color")
47
+ @typechecked
48
+ async def reset_color() -> JSONResponse:
49
+ log.info("POST /api/debugger/reset_color")
50
+ scanned_dir = update_debug_toggles(save_to_file=False)
51
+ scanned_dir.reset_colors()
52
+ output = dir_to_output_format(scanned_dir)
53
+ return JSONResponse(content=output)
54
+
55
+
56
+ @debugger.router.post("/reset_emoji")
57
+ @typechecked
58
+ async def reset_emoji() -> JSONResponse:
59
+ log.info("POST /api/debugger/reset_emoji")
60
+ scanned_dir = update_debug_toggles(save_to_file=False)
61
+ scanned_dir.reset_emojis()
62
+ output = dir_to_output_format(scanned_dir)
63
+ return JSONResponse(content=output)
64
+
65
+
66
+ @debugger.router.get("/root_dir")
67
+ @typechecked
68
+ async def get_root() -> JSONResponse:
69
+ log.info("GET /api/debugger/root_dir")
70
+ return JSONResponse(content={"root_dir": get_root_dir()})
71
+
72
+
73
+ @debugger.router.post("/root_dir")
74
+ @typechecked
75
+ async def set_root(data: dict) -> JSONResponse:
76
+ log.info("POST /api/debugger/root_dir")
77
+ path = data.get("root_dir", "")
78
+ if not path or not os.path.isdir(path):
79
+ return JSONResponse(content={"error": "Invalid directory path"}, status_code=400)
80
+ set_root_dir(path)
81
+ with open(NEEDS_RESYNC_FILE, 'w') as f:
82
+ f.write('1')
83
+ return JSONResponse(content={"root_dir": get_root_dir()})
File without changes
@@ -0,0 +1,33 @@
1
+ import logging
2
+ from backend.config.Apps import SubApp
3
+ from contextlib import asynccontextmanager
4
+ from fastapi.responses import PlainTextResponse
5
+ from typeguard import typechecked
6
+ from fastapi import status
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+ @asynccontextmanager
11
+ async def health_lifespan():
12
+ log.debug("health_lifespan START")
13
+ yield
14
+ log.debug("health_lifespan END")
15
+
16
+ health = SubApp("health", health_lifespan)
17
+
18
+ ######################################
19
+ # Health Check Endpoints #
20
+ ######################################
21
+
22
+ @health.router.get("/check")
23
+ @typechecked
24
+ async def check() -> PlainTextResponse:
25
+ log.info("Health check successful")
26
+ return PlainTextResponse(
27
+ content="OK",
28
+ status_code=status.HTTP_200_OK,
29
+ headers={
30
+ "Content-Type": "text/plain",
31
+ "Content-Length": "2"
32
+ }
33
+ )
backend/config/Apps.py ADDED
@@ -0,0 +1,48 @@
1
+ import logging
2
+ import os
3
+ from fastapi import FastAPI, APIRouter
4
+ from uuid import uuid4
5
+ from typing import List
6
+ from contextlib import asynccontextmanager
7
+ from contextlib import AsyncExitStack
8
+ from typing import Callable
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ class SubApp:
14
+ def __init__(self, name:str, lifespan:Callable):
15
+ log.debug("SubApp.__init__ START: %s", name)
16
+ self.id = uuid4()
17
+ self.name = name
18
+ self.prefix = f"/api/{name}"
19
+ self.lifespan = lifespan
20
+ self.router = APIRouter()
21
+ log.debug("SubApp.__init__ END")
22
+
23
+ def __str__(self):
24
+ return f"SubApp(name={self.name}, prefix={self.prefix}, id={self.id})"
25
+
26
+ class MainApp:
27
+ def __init__(self, sub_apps: List[SubApp]):
28
+ log.debug("MainApp.__init__ START")
29
+
30
+ @asynccontextmanager
31
+ async def lifespan(app: FastAPI):
32
+ async with AsyncExitStack() as stack:
33
+ for sub_app in sub_apps:
34
+ log.debug("Starting lifespan for sub_app: %s", sub_app.name)
35
+ await stack.enter_async_context(sub_app.lifespan())
36
+ port = os.environ.get("BACKEND_PORT", "8324")
37
+ print(f"\nCheck out the API docs at: http://127.0.0.1:{port}/docs\n")
38
+ yield
39
+
40
+ self.app = FastAPI(lifespan=lifespan)
41
+
42
+ for sub_app in sub_apps:
43
+ self.app.include_router(
44
+ sub_app.router,
45
+ prefix=sub_app.prefix,
46
+ tags=[sub_app.name]
47
+ )
48
+ log.debug("MainApp.__init__ END")
File without changes
@@ -0,0 +1,29 @@
1
+ import os
2
+ from backend.core.data_dir import TOGGLE_FILE, ROOT_DIR_FILE
3
+
4
+ DEFAULT_COLOR = '#ffffff'
5
+ DEFAULT_TOGGLED = False
6
+ DEFAULT_SET_MANUALLY = False
7
+ DEFAULT_SET_MANUALLY_EMOJI = False
8
+ DEFAULT_EMOJI = '⚫'
9
+
10
+
11
+ def get_root_dir() -> str:
12
+ """Priority: env var > persisted file > cwd."""
13
+ env = os.environ.get("SWARM_DEBUG_ROOT")
14
+ if env:
15
+ return os.path.abspath(env)
16
+
17
+ if os.path.exists(ROOT_DIR_FILE):
18
+ with open(ROOT_DIR_FILE, "r") as f:
19
+ saved = f.read().strip()
20
+ if saved:
21
+ return saved
22
+
23
+ return os.getcwd()
24
+
25
+
26
+ def set_root_dir(path: str):
27
+ path = os.path.abspath(path)
28
+ with open(ROOT_DIR_FILE, "w") as f:
29
+ f.write(path)
@@ -0,0 +1,87 @@
1
+ # Haik: sorry bout the filename
2
+
3
+ import threading
4
+ from backend.core.models.project_scanner import update_debug_toggles
5
+ from backend.core.models.Directory import Directory
6
+ from backend.core.models.DebugFile import DebugFile
7
+ from backend.core.DEFAULTS import DEFAULT_COLOR, DEFAULT_TOGGLED, DEFAULT_EMOJI
8
+ from backend.core.data_dir import NEEDS_RESYNC_FILE
9
+ import os
10
+ import time
11
+
12
+ class Debugleton:
13
+ _instance = None
14
+ _lock = threading.Lock() # Lock for thread-safe singleton creation
15
+ sync_lock: threading.Lock
16
+
17
+ def __new__(cls, *args, **kwargs):
18
+ # Double-checked locking for thread-safe singleton creation
19
+ if cls._instance is None:
20
+ with cls._lock:
21
+ if cls._instance is None:
22
+ cls._instance = super(Debugleton, cls).__new__(cls)
23
+ print("\033[38;5;120m\n---------------------------------\033[0m")
24
+ print("\033[38;5;120m|\tDEBUGLETON INIT \t|\033[0m")
25
+ cls._instance.dir = None
26
+ print("\033[38;5;120m|\tScanning Project...\t|\033[0m")
27
+ cls._instance.sync_lock = threading.Lock()
28
+ cls._instance.sync_lock.acquire(blocking=False)
29
+ cls._instance.sync_to_saved(is_first_sync=True)
30
+ cls._instance.sync_lock.release()
31
+ print("\033[38;5;120m|\t...Project Scanned\t|\033[0m")
32
+ print("\033[38;5;120m|\tDEBUGLETON INIT DONE\t|\033[0m")
33
+ print("\033[38;5;120m---------------------------------\n\033[0m")
34
+ # else: print("DEBUGLETON Already initialized INNER")
35
+ # else: print("DEBUGLETON Already initialized OUTER")
36
+ return cls._instance
37
+
38
+
39
+ def sync_to_saved(self, is_first_sync=False):
40
+ # print(f"[sync_to_saved]: START")
41
+ if not is_first_sync: self.sync_lock.acquire()
42
+ # print(f"[sync_to_saved]: Acquired sync lock")
43
+ self.dir = update_debug_toggles(save_to_file=False)
44
+ # print(f"Synced to saved dir: {self.dir}")
45
+ self.abspaths, self.instances = self.dir.get_ordered_abspaths_and_instances()
46
+ # print(f"Synced to abspaths: {self.abspaths}")
47
+ with open(NEEDS_RESYNC_FILE, 'w') as f:
48
+ f.write('0')
49
+ if not is_first_sync: self.sync_lock.release()
50
+ # print(f"[sync_to_saved]: Released sync lock")
51
+ # print(f"[sync_to_saved]: END")
52
+
53
+ def needs_resync(self):
54
+ # print(f"[needs_resync]: START")
55
+ num_tries = 0
56
+ while self.is_syncing():
57
+ print(f"Waiting for Debugleton to sync... ({num_tries})")
58
+ time.sleep(5)
59
+ num_tries += 1
60
+ if num_tries > 10:
61
+ print(f"""
62
+ NOTE: Debugleton is taking a long time, there's one scenario where it breaks:
63
+ \n\t- If running in docker, and you deleted one of the root dirs in the volumes of docker compose,
64
+ \n\t then the debugger will not be able to find the project and will get stuck in an infinite loop.
65
+ \n\t- In this case, you can restart the docker container and delete the volume in the docker compose file and it will resync.
66
+ """)
67
+ with open(NEEDS_RESYNC_FILE, 'r') as f:
68
+ does_need_resync = True if f.read().strip() == '1' else False
69
+ # if does_need_resync: print("Resyncing Debugleton...")
70
+ # print(f"[needs_resync]: END")
71
+ return does_need_resync
72
+
73
+ def is_syncing(self):
74
+ return self.sync_lock.locked()
75
+
76
+ def find_file_info(self, filepath: str):
77
+ filepath = filepath.lower()
78
+ # print(f"Finding file info for {filepath}")
79
+ if self.needs_resync():
80
+ self.sync_to_saved()
81
+ try:
82
+ filepath_id = self.abspaths.index(filepath)
83
+ match = self.instances[filepath_id]
84
+ return match.color, match.is_toggled, match.emoji
85
+ except ValueError:
86
+ print(f"Filepath not found: {filepath}")
87
+ return DEFAULT_COLOR, DEFAULT_TOGGLED, DEFAULT_EMOJI
File without changes
@@ -0,0 +1,30 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ DATA_DIR = os.path.join(str(Path.home()), ".swarm-debug")
5
+
6
+ _DEFAULTS = {
7
+ "debug_toggles.json": "[]",
8
+ "log_mode.txt": "all",
9
+ "needs_resync.txt": "1",
10
+ "root_dir.txt": "",
11
+ }
12
+
13
+
14
+ def _ensure_data_dir():
15
+ os.makedirs(DATA_DIR, exist_ok=True)
16
+
17
+
18
+ def get_data_file(name: str) -> str:
19
+ _ensure_data_dir()
20
+ path = os.path.join(DATA_DIR, name)
21
+ if not os.path.exists(path) and name in _DEFAULTS:
22
+ with open(path, "w", encoding="utf-8") as f:
23
+ f.write(_DEFAULTS[name])
24
+ return path
25
+
26
+
27
+ TOGGLE_FILE = get_data_file("debug_toggles.json")
28
+ NEEDS_RESYNC_FILE = get_data_file("needs_resync.txt")
29
+ LOG_MODE_FILE = get_data_file("log_mode.txt")
30
+ ROOT_DIR_FILE = get_data_file("root_dir.txt")
@@ -0,0 +1,2 @@
1
+ from backend.core.log.log_config import LogConfig, log_config
2
+ from backend.core.log.log_mode import get_log_mode, set_log_mode
@@ -0,0 +1,46 @@
1
+ import logging
2
+ from backend.core.log.log_mode import get_log_mode, set_log_mode
3
+
4
+ class LogConfig:
5
+ _instance = None
6
+ MODES = {
7
+ "all": 1,
8
+ "debug": 10,
9
+ "test": 20,
10
+ }
11
+
12
+ def __new__(cls):
13
+ if cls._instance is None:
14
+ cls._instance = super(LogConfig, cls).__new__(cls)
15
+ cls._instance._initialize_logger()
16
+ return cls._instance
17
+
18
+ def _initialize_logger(self):
19
+ for name, level in self.MODES.items():
20
+ logging.addLevelName(level, name.upper())
21
+ self.logger = logging.getLogger('custom_logger')
22
+ self.logger.propagate = False # Prevent log propagation
23
+ handler = logging.StreamHandler()
24
+ formatter = logging.Formatter('%(message)s')
25
+ handler.setFormatter(formatter)
26
+
27
+ # Remove existing handlers to prevent duplicate logging
28
+ if self.logger.hasHandlers():
29
+ self.logger.handlers.clear()
30
+
31
+ self.logger.addHandler(handler)
32
+ self.set_debug_mode(get_log_mode())
33
+
34
+ def debug_custom(self, message, mode = None, *args, **kwargs):
35
+ if mode is None:
36
+ mode = get_log_mode()
37
+ if self.logger.isEnabledFor(self.MODES[mode]):
38
+ self.logger._log(self.MODES[mode], message, args, **kwargs)
39
+
40
+ def set_debug_mode(self, mode):
41
+ # print(f"Setting debug mode from {current_mode} -> to {mode}")
42
+ if mode not in self.MODES: raise ValueError(f"Invalid mode: {mode}")
43
+ set_log_mode(mode)
44
+ self.logger.setLevel(self.MODES[mode])
45
+
46
+ log_config = LogConfig()
@@ -0,0 +1,13 @@
1
+ import os
2
+ from backend.core.data_dir import LOG_MODE_FILE
3
+
4
+
5
+ def set_log_mode(mode):
6
+ with open(LOG_MODE_FILE, 'w') as f:
7
+ f.write(mode)
8
+
9
+ def get_log_mode():
10
+ if os.path.exists(LOG_MODE_FILE):
11
+ with open(LOG_MODE_FILE, 'r') as f:
12
+ return f.read().strip()
13
+ return 'all' # Default to 'all' if the file doesn't exist
@@ -0,0 +1,28 @@
1
+ import os
2
+ from backend.core.models.File import File
3
+ from backend.core.DEFAULTS import DEFAULT_COLOR, DEFAULT_TOGGLED, DEFAULT_SET_MANUALLY, DEFAULT_SET_MANUALLY_EMOJI, DEFAULT_EMOJI
4
+
5
+ class DebugFile(File):
6
+ def __init__(self, filename, path, color=DEFAULT_COLOR, is_toggled=DEFAULT_TOGGLED,
7
+ set_manually=DEFAULT_SET_MANUALLY, set_manually_emoji=DEFAULT_SET_MANUALLY_EMOJI,
8
+ emoji=DEFAULT_EMOJI, directory=None):
9
+ super().__init__(filename, path)
10
+ self.color = color
11
+ self.is_toggled = is_toggled
12
+ self.set_manually = set_manually
13
+ self.set_manually_emoji = set_manually_emoji
14
+ self.emoji = emoji
15
+ self.directory = directory # Reference to parent directory
16
+
17
+ def to_dict(self):
18
+ """
19
+ Converts the DebugFile object to a dictionary format.
20
+ """
21
+ return {
22
+ "name": os.path.basename(self.filename),
23
+ "color": self.color,
24
+ "is_toggled": self.is_toggled,
25
+ "set_manually": self.set_manually,
26
+ "set_manually_emoji": self.set_manually_emoji,
27
+ "emoji": self.emoji
28
+ }
@@ -0,0 +1,216 @@
1
+ import os
2
+ import colorsys
3
+ from backend.core.models.DebugFile import DebugFile
4
+ from backend.core.DEFAULTS import DEFAULT_COLOR, DEFAULT_TOGGLED, DEFAULT_SET_MANUALLY, DEFAULT_SET_MANUALLY_EMOJI, DEFAULT_EMOJI, get_root_dir
5
+ from backend.core.utils.path_mngr import get_abspath, get_root_rel_path
6
+
7
+ class Directory:
8
+ def __init__(self, path, color=DEFAULT_COLOR, is_toggled=DEFAULT_TOGGLED,
9
+ set_manually=DEFAULT_SET_MANUALLY, set_manually_emoji=DEFAULT_SET_MANUALLY_EMOJI,
10
+ emoji=DEFAULT_EMOJI):
11
+ self.path = path
12
+ # print(f"Directory init: {self.path}")
13
+ self.children = [] # Can contain DebugFile or other Directory objects
14
+ self.color = color
15
+ self.is_toggled = is_toggled
16
+ self.set_manually = set_manually
17
+ self.set_manually_emoji = set_manually_emoji
18
+ self.emoji = emoji
19
+
20
+ def __str__(self):
21
+ return f"Directory: {self.path}\nNum Children: {len(self.children)}\nColor: {self.color}\nToggled: {self.is_toggled}\nSet Manually: {self.set_manually}"
22
+
23
+ def get_abspath(self):
24
+ return get_abspath(self.path)
25
+
26
+ def add_child(self, child):
27
+ """
28
+ Adds a child to the directory (either a DebugFile or another Directory).
29
+ """
30
+ self.children.append(child)
31
+
32
+ def get_ordered_abspaths_and_instances(self):
33
+ # print("[get_ordered_abspaths]: START")
34
+ root_dir = get_root_dir()
35
+ # print(f"[get_ordered_abspaths]: Curr path: {curr_file_path}")
36
+ # print(f"[get_ordered_abspaths]: Dir path: {root_dir}")
37
+ def construct_ordered_abspaths(dir: Directory, ordered_abspaths: list):
38
+ dir_path = dir.path
39
+ full_path = os.path.join(root_dir, dir_path)
40
+ ordered_abspaths.append({"abspath": full_path, "instance": dir})
41
+ # print(f"\t[construct_ordered_abspaths]: Full path: {full_path}")
42
+ for child in dir.children:
43
+ child_abspath = os.path.join(root_dir, child.path).lower()
44
+ if os.path.isdir(child_abspath):
45
+ construct_ordered_abspaths(child, ordered_abspaths)
46
+ elif os.path.isfile(child_abspath):
47
+ # print(f"\t[construct_ordered_abspaths]: Child is file: {child_abspath}")
48
+ ordered_abspaths.append({"abspath": child_abspath, "instance": child})
49
+ else:
50
+ print(f"\033[38;5;120mEntry is non existent: {child_abspath}\033[0m")
51
+ # print(f"\t[construct_ordered_abspaths]: Finished for dir: {full_path}")
52
+ # print(f"\t[construct_ordered_abspaths]: RETURNING FROM DIR: {full_path}")
53
+ return ordered_abspaths
54
+ ordered_abspaths_and_instances = construct_ordered_abspaths(self, [])
55
+ # print("[get_ordered_abspaths]: Finished getting ordered abspaths and instances")
56
+ # for abspath_and_instance in ordered_abspaths_and_instances:
57
+ # abspath = abspath_and_instance["abspath"]
58
+ # print(f"\t[get_ordered_abspaths]: Abspath: {abspath}")
59
+ ordered_abspaths = [abspath_and_instance["abspath"] for abspath_and_instance in ordered_abspaths_and_instances]
60
+ ordered_instances = [abspath_and_instance["instance"] for abspath_and_instance in ordered_abspaths_and_instances]
61
+ return ordered_abspaths, ordered_instances
62
+
63
+
64
+ def build_structure(self):
65
+ print("[build_structure]: START")
66
+ root_dir = self.get_abspath()
67
+ # print(f"[build_structure]: Root dir: {root_dir}")
68
+ excluded_dirs = [".venv", "debugger", "node_modules", ".git", "__pycache__"]
69
+ project_structure = []
70
+
71
+ def construct_project_structure(dir_path: str, parent_dir: Directory):
72
+ # print(f"[build_structure]: Scanning dir: {dir_path}")
73
+ with os.scandir(dir_path) as it:
74
+ for entry in it:
75
+ # print(f"[build_structure]: Entry: {entry.path}")
76
+ if any(excluded_dir in entry.path for excluded_dir in excluded_dirs):
77
+ # print(f"[build_structure]: Excluding {entry.path}")
78
+ continue
79
+ root_rel_path = get_root_rel_path(entry.path)
80
+ if entry.is_dir():
81
+ subdir = Directory(root_rel_path)
82
+ construct_project_structure(entry.path, subdir)
83
+ parent_dir.add_child(subdir)
84
+ elif entry.is_file():
85
+ debug_file = DebugFile(filename=entry.name, path=root_rel_path)
86
+ if debug_file.calls_debug_function():
87
+ parent_dir.add_child(debug_file)
88
+ else:
89
+ raise Exception(f"[build_structure]: Entry is not dir or file: {entry.path}")
90
+
91
+ construct_project_structure(root_dir, self)
92
+ # [print(f"[build_structure]: {file}") for file in project_structure]
93
+ # print(f"[build_structure]: END")
94
+ return
95
+
96
+ def to_dict(self):
97
+ """
98
+ Converts the Directory object to a dictionary format, recursively.
99
+ """
100
+ return {
101
+ "name": os.path.basename(self.path),
102
+ "color": self.color,
103
+ "is_toggled": self.is_toggled,
104
+ "set_manually": self.set_manually,
105
+ "set_manually_emoji": self.set_manually_emoji,
106
+ "emoji": self.emoji,
107
+ "children": [child.to_dict() if isinstance(child, DebugFile) else child.to_dict() for child in self.children]
108
+ }
109
+
110
+ def prune_empty(self):
111
+ # Recursively prune empty directories
112
+ # Base case) if the current directory has no children, return
113
+ # Recursive case) for each of the directories in the current directory, call prune_empty
114
+ # then remove the directory from the children of the current directory if it has no children
115
+ for child in self.children[:]:
116
+ if isinstance(child, Directory):
117
+ # Recursively prune empty subdirectories
118
+ child.prune_empty()
119
+ # If the subdirectory is empty after pruning, remove it
120
+ if len(child.children) == 0:
121
+ self.children.remove(child)
122
+
123
+ def propagate_toggled_state(self):
124
+ """
125
+ Propagates the toggled state down the hierarchy.
126
+ """
127
+ for child in self.children:
128
+ if isinstance(child, DebugFile) and not child.set_manually:
129
+ child.is_toggled = self.is_toggled
130
+ elif isinstance(child, Directory) and not child.set_manually:
131
+ child.is_toggled = self.is_toggled
132
+ child.propagate_toggled_state()
133
+
134
+ def propagate_color(self, parent_color=DEFAULT_COLOR):
135
+ """
136
+ Propagates the color from parent to children.
137
+ """
138
+ if self.color == DEFAULT_COLOR:
139
+ self.color = lighten_color(parent_color)
140
+ for child in self.children:
141
+ if isinstance(child, DebugFile) and child.color == DEFAULT_COLOR:
142
+ child.color = lighten_color(self.color)
143
+ elif isinstance(child, Directory):
144
+ child.propagate_color(self.color)
145
+
146
+ def load_from_json(self, json_data):
147
+ """
148
+ Loads a directory structure from a JSON file into this Directory instance.
149
+ """
150
+ for item in json_data:
151
+ if 'children' in item:
152
+ subdir = Directory(
153
+ path=os.path.join(self.path, item['name']),
154
+ color=item.get('color', DEFAULT_COLOR),
155
+ is_toggled=item.get('is_toggled', DEFAULT_TOGGLED),
156
+ set_manually=item.get('set_manually', DEFAULT_SET_MANUALLY),
157
+ set_manually_emoji=item.get('set_manually_emoji', DEFAULT_SET_MANUALLY_EMOJI),
158
+ emoji=item.get('emoji', DEFAULT_EMOJI)
159
+ )
160
+
161
+ subdir.load_from_json(item['children'])
162
+ self.add_child(subdir)
163
+ else:
164
+ debug_file = DebugFile(
165
+ filename=item['name'],
166
+ path=os.path.join(self.path, item['name']),
167
+ color=item.get('color', DEFAULT_COLOR),
168
+ is_toggled=item.get('is_toggled', DEFAULT_TOGGLED),
169
+ set_manually=item.get('set_manually', DEFAULT_SET_MANUALLY),
170
+ set_manually_emoji=item.get('set_manually_emoji', DEFAULT_SET_MANUALLY_EMOJI),
171
+ emoji=item.get('emoji', DEFAULT_EMOJI),
172
+ directory=self
173
+ )
174
+ self.add_child(debug_file)
175
+
176
+ def reset_colors(self):
177
+ """
178
+ Resets the color and set_manually flag of all nodes to defaults.
179
+ """
180
+ self.color = DEFAULT_COLOR
181
+ self.set_manually = DEFAULT_SET_MANUALLY
182
+ for child in self.children:
183
+ if isinstance(child, DebugFile):
184
+ child.color = DEFAULT_COLOR
185
+ child.set_manually = DEFAULT_SET_MANUALLY
186
+ elif isinstance(child, Directory):
187
+ child.reset_colors()
188
+
189
+ def reset_emojis(self):
190
+ """
191
+ Resets the emoji and set_manually_emoji flag of all nodes to defaults.
192
+ """
193
+ self.emoji = DEFAULT_EMOJI
194
+ self.set_manually_emoji = DEFAULT_SET_MANUALLY_EMOJI
195
+ for child in self.children:
196
+ if isinstance(child, DebugFile):
197
+ child.emoji = DEFAULT_EMOJI
198
+ child.set_manually_emoji = DEFAULT_SET_MANUALLY_EMOJI
199
+ elif isinstance(child, Directory):
200
+ child.reset_emojis()
201
+
202
+
203
+ def lighten_color(color, amount=0.1):
204
+ """
205
+ Lightens the given color by the specified amount.
206
+ """
207
+ try:
208
+ color = color.lstrip('#')
209
+ r, g, b = int(color[:2], 16), int(color[2:4], 16), int(color[4:6], 16)
210
+ h, l, s = colorsys.rgb_to_hls(r / 255.0, g / 255.0, b / 255.0)
211
+ l = min(1, l + amount)
212
+ r, g, b = colorsys.hls_to_rgb(h, l, s)
213
+ return '#{:02x}{:02x}{:02x}'.format(int(r * 255), int(g * 255), int(b * 255))
214
+ except Exception as e:
215
+ print(f"Error lightening color {color}: {e}")
216
+ return color
@@ -0,0 +1,29 @@
1
+ import os
2
+ from backend.core.utils.path_mngr import get_abspath
3
+
4
+ class File:
5
+ def __init__(self, filename, path):
6
+ self.filename = filename
7
+ self.path = path
8
+
9
+ def get_abspath(self):
10
+ return get_abspath(self.path)
11
+
12
+ def calls_debug_function(self):
13
+ """
14
+ Checks if the file calls the debug function.
15
+ """
16
+ full_path = self.get_abspath()
17
+
18
+ if not full_path.endswith('.py') or full_path.endswith('.pyc'):
19
+ result = False
20
+ else:
21
+ try:
22
+ with open(full_path, 'r', encoding='utf-8') as file:
23
+ content = file.read()
24
+ result = 'debug(' in content
25
+ except (UnicodeDecodeError, FileNotFoundError) as e:
26
+ print(f"Error reading file {full_path}")
27
+ result = False
28
+ # print(f"??calls_debug_function?? {result}")
29
+ return result
@@ -0,0 +1,4 @@
1
+ from backend.core.models.File import File
2
+ from backend.core.models.DebugFile import DebugFile
3
+ from backend.core.models.Directory import Directory
4
+ from backend.core.models.project_scanner import update_debug_toggles, dir_to_output_format