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 +0 -0
- backend/apps/__init__.py +0 -0
- backend/apps/debugger/__init__.py +0 -0
- backend/apps/debugger/debugger.py +83 -0
- backend/apps/health/__init__.py +0 -0
- backend/apps/health/health.py +33 -0
- backend/config/Apps.py +48 -0
- backend/config/__init__.py +0 -0
- backend/core/DEFAULTS.py +29 -0
- backend/core/Debugleton.py +87 -0
- backend/core/__init__.py +0 -0
- backend/core/data_dir.py +30 -0
- backend/core/log/__init__.py +2 -0
- backend/core/log/log_config.py +46 -0
- backend/core/log/log_mode.py +13 -0
- backend/core/models/DebugFile.py +28 -0
- backend/core/models/Directory.py +216 -0
- backend/core/models/File.py +29 -0
- backend/core/models/__init__.py +4 -0
- backend/core/models/project_scanner.py +153 -0
- backend/core/utils/__init__.py +3 -0
- backend/core/utils/color_adjuster.py +13 -0
- backend/core/utils/debug_arg_parser.py +22 -0
- backend/core/utils/path_mngr.py +15 -0
- backend/debugger_gui_build/bundle.js +102 -0
- backend/debugger_gui_build/bundle.js.LICENSE.txt +68 -0
- backend/debugger_gui_build/index.html +1 -0
- backend/main.py +40 -0
- debug.py +57 -0
- swarm_debug-0.1.0.dist-info/METADATA +237 -0
- swarm_debug-0.1.0.dist-info/RECORD +34 -0
- swarm_debug-0.1.0.dist-info/WHEEL +5 -0
- swarm_debug-0.1.0.dist-info/entry_points.txt +2 -0
- swarm_debug-0.1.0.dist-info/top_level.txt +2 -0
backend/__init__.py
ADDED
|
File without changes
|
backend/apps/__init__.py
ADDED
|
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
|
backend/core/DEFAULTS.py
ADDED
|
@@ -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
|
backend/core/__init__.py
ADDED
|
File without changes
|
backend/core/data_dir.py
ADDED
|
@@ -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,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
|