@@ -34,6 +34,7 @@ class ContainerMetrics:
3434@dataclass
3535class FileState :
3636 """Tracks the state of a file for change detection."""
37+
3738 path : Path
3839 size : int
3940 mtime : float
@@ -46,21 +47,17 @@ class DockerExecutor:
4647
4748 WORK_DIR = "/mnt/data" # Working directory will be the same as data mount point
4849 DATA_MOUNT = "/mnt/data" # Mount point for session data
49-
50+
5051 # Language-specific execution commands
5152 LANGUAGE_EXECUTORS = {
5253 "py" : ["python" , "-c" ],
5354 "r" : ["Rscript" , "-e" ],
5455 }
55-
56+
5657 # Language-specific messages
5758 LANGUAGE_SPECIFIC_MESSAGES = {
58- "py" : {
59- "empty_output" : "Empty. Make sure to explicitly print() the results in Python"
60- },
61- "r" : {
62- "empty_output" : "Empty. Make sure to use print() or cat() to display results in R"
63- }
59+ "py" : {"empty_output" : "Empty. Make sure to explicitly print() the results in Python" },
60+ "r" : {"empty_output" : "Empty. Make sure to use print() or cat() to display results in R" },
6461 }
6562
6663 def __init__ (self ):
@@ -113,63 +110,56 @@ def _scan_directory(self, directory: Path) -> Dict[str, FileState]:
113110 Returns a dictionary mapping relative file paths to their FileState objects.
114111 """
115112 file_states = {}
116-
113+
117114 if not directory .exists ():
118115 logger .warning (f"Directory { directory } does not exist" )
119116 return file_states
120-
117+
121118 # Walk through the directory recursively
122119 for root , _ , files in os .walk (directory ):
123120 root_path = Path (root )
124-
121+
125122 # Compute relative path from the base directory
126123 rel_root = root_path .relative_to (directory )
127-
124+
128125 for filename in files :
129126 # Skip lock files
130- if filename .endswith (' .lock' ):
127+ if filename .endswith (" .lock" ):
131128 continue
132-
129+
133130 file_path = root_path / filename
134-
131+
135132 # Compute relative path for dictionary key
136- if rel_root == Path ('.' ):
133+ if rel_root == Path ("." ):
137134 rel_path = filename
138135 else :
139136 rel_path = str (rel_root / filename )
140-
137+
141138 try :
142139 # Get file stats
143140 stat = file_path .stat ()
144141 size = stat .st_size
145142 mtime = stat .st_mtime
146-
143+
147144 # Calculate MD5 hash for content comparison
148145 md5_hash = hashlib .md5 (file_path .read_bytes ()).hexdigest ()
149-
146+
150147 # Store file state
151- file_states [rel_path ] = FileState (
152- path = file_path ,
153- size = size ,
154- mtime = mtime ,
155- md5_hash = md5_hash
156- )
148+ file_states [rel_path ] = FileState (path = file_path , size = size , mtime = mtime , md5_hash = md5_hash )
157149 logger .debug (f"Scanned file: { rel_path } , size: { size } , hash: { md5_hash } " )
158150 except (PermissionError , FileNotFoundError ) as e :
159151 logger .warning (f"Error scanning file { file_path } : { str (e )} " )
160152 continue
161-
153+
162154 return file_states
163155
164- def _find_changed_files (self ,
165- before_states : Dict [str , FileState ],
166- after_states : Dict [str , FileState ]) -> Set [str ]:
156+ def _find_changed_files (self , before_states : Dict [str , FileState ], after_states : Dict [str , FileState ]) -> Set [str ]:
167157 """
168158 Compare before and after file states to identify new or modified files.
169159 Returns a set of relative paths of changed files.
170160 """
171161 changed_files = set ()
172-
162+
173163 # Find new or modified files
174164 for rel_path , after_state in after_states .items ():
175165 if rel_path not in before_states :
@@ -179,20 +169,23 @@ def _find_changed_files(self,
179169 else :
180170 before_state = before_states [rel_path ]
181171 # Check if file was modified (size, hash, or timestamp changed)
182- if (before_state .size != after_state .size or
183- before_state .md5_hash != after_state .md5_hash ):
184- logger .info (f"Modified file detected: { rel_path } , before={ before_state .size } :{ before_state .md5_hash } , after={ after_state .size } :{ after_state .md5_hash } " )
172+ if before_state .size != after_state .size or before_state .md5_hash != after_state .md5_hash :
173+ logger .info (
174+ f"Modified file detected: { rel_path } , before={ before_state .size } :{ before_state .md5_hash } , after={ after_state .size } :{ after_state .md5_hash } "
175+ )
185176 changed_files .add (rel_path )
186177 else :
187178 logger .info (f"Unchanged file: { rel_path } , size={ after_state .size } , hash={ after_state .md5_hash } " )
188-
179+
189180 # Add debug logs for summarizing scan results
190181 for rel_path in before_states :
191182 if rel_path not in after_states :
192183 logger .info (f"File deleted: { rel_path } " )
193-
194- logger .info (f"Before scan: { len (before_states )} files, After scan: { len (after_states )} files, Changed: { len (changed_files )} files" )
195-
184+
185+ logger .info (
186+ f"Before scan: { len (before_states )} files, After scan: { len (after_states )} files, Changed: { len (changed_files )} files"
187+ )
188+
196189 return changed_files
197190
198191 async def _update_container_metrics (self , container ) -> None :
@@ -274,10 +267,11 @@ async def execute(
274267 session_id : str ,
275268 lang : Literal ["py" , "r" ],
276269 files : Optional [List [Dict [str , Any ]]] = None ,
277- timeout : int = 30 ,
270+ config : Optional [ Dict [ str , Any ]] = None ,
278271 ) -> Dict [str , Any ]:
279272 """Execute code in a Docker container with file management."""
280273 container = None
274+ config = config or {}
281275
282276 try :
283277 # Ensure Docker client is initialized and valid
@@ -355,14 +349,24 @@ async def execute(
355349 logger .error (f"Error checking for image { image_name } : { str (e )} " )
356350 raise
357351
352+ # Get container configuration, with provided config overriding settings
353+ memory_limit_mb = config .get ("memory_limit_mb" , settings .CONTAINER_MEMORY_LIMIT_MB )
354+ cpu_limit = config .get ("cpu_limit" , settings .CONTAINER_CPU_LIMIT )
355+ network_enabled = config .get ("network_enabled" , settings .DOCKER_NETWORK_ENABLED )
356+
357+ logger .info (
358+ f"Container config - Memory: { memory_limit_mb } MB, CPU: { cpu_limit } , Network: { network_enabled } "
359+ )
360+
358361 # Create container config
359362 config = {
360363 "Image" : image_name ,
361364 "Cmd" : ["sleep" , "infinity" ],
362365 "WorkingDir" : self .WORK_DIR ,
363- "NetworkDisabled" : True ,
366+ "NetworkDisabled" : not network_enabled ,
364367 "HostConfig" : {
365- "Memory" : 512 * 1024 * 1024 , # 512MB in bytes
368+ "Memory" : memory_limit_mb * 1024 * 1024 , # Convert MB to bytes
369+ "NanoCpus" : int (cpu_limit * 1e9 ), # Convert CPU cores to nano CPUs
366370 "Mounts" : [
367371 {
368372 "Type" : "bind" ,
@@ -414,11 +418,11 @@ async def execute(
414418 # Execute the code with the appropriate interpreter
415419 logger .info (f"Code to execute: { code } " )
416420 logger .info (f"Language: { lang } " )
417-
421+
418422 # Get the execution command for the specified language
419423 exec_cmd = self .LANGUAGE_EXECUTORS .get (lang , self .LANGUAGE_EXECUTORS ["py" ])
420424 logger .info (f"Using execution command: { exec_cmd } " )
421-
425+
422426 # Execute the code with the appropriate interpreter
423427 exec = await container .exec (cmd = [* exec_cmd , code ], user = "jovyan" , stdout = True , stderr = True )
424428 # Use raw API call to get output
@@ -450,7 +454,7 @@ async def execute(
450454 output_files = []
451455 existing_filenames = {file ["name" ] for file in (files or [])}
452456 logger .info (f"Existing filenames: { existing_filenames } " )
453-
457+
454458 for rel_path in changed_file_paths :
455459 file_path = session_path / rel_path
456460 if file_path .is_file ():
@@ -466,7 +470,7 @@ async def execute(
466470 # Use directory structure in filepath if present
467471 filepath = f"{ session_id } /{ rel_path } "
468472 filename = Path (rel_path ).name
469-
473+
470474 file_data = {
471475 "id" : file_id ,
472476 "session_id" : session_id ,
0 commit comments