11from __future__ import annotations
22
33import datetime
4- import typing
54from collections import defaultdict
6- from typing import List
5+ from typing import TYPE_CHECKING , List , Optional
76
87from flask import current_app
9- from sqlalchemy import PickleType , and_
8+ from sqlalchemy import ForeignKey , PickleType , Text , and_
9+ from sqlalchemy .orm import Mapped , mapped_column , relationship
1010
1111from ref import db
1212
1313from .enums import ExerciseBuildStatus
14- from .instance import Instance , Submission
1514from .util import CommonDbOpsMixin , ModelToStringMixin
1615
16+ if TYPE_CHECKING :
17+ from .instance import Instance , InstanceService , Submission
18+
1719
1820class ConfigParsingError (Exception ):
19- def __init__ (self , msg : str , path : str = None ):
21+ def __init__ (self , msg : str , path : Optional [ str ] = None ):
2022 if path :
2123 msg = f"{ msg } ({ path } )"
2224 super ().__init__ (msg )
@@ -33,16 +35,17 @@ class RessourceLimits(CommonDbOpsMixin, ModelToStringMixin, db.Model):
3335 "memory_kernel_in_mb" ,
3436 ]
3537 __tablename__ = "exercise_ressource_limits"
36- id = db .Column (db .Integer , primary_key = True )
3738
38- cpu_cnt_max : float = db .Column (db .Float (), nullable = True , default = None )
39- cpu_shares : int = db .Column (db .Integer (), nullable = True , default = None )
39+ id : Mapped [int ] = mapped_column (primary_key = True )
40+
41+ cpu_cnt_max : Mapped [Optional [float ]] = mapped_column (default = None )
42+ cpu_shares : Mapped [Optional [int ]] = mapped_column (default = None )
4043
41- pids_max : int = db . Column ( db . Integer (), nullable = True , default = None )
44+ pids_max : Mapped [ Optional [ int ]] = mapped_column ( default = None )
4245
43- memory_in_mb : int = db . Column ( db . Integer (), nullable = True , default = None )
44- memory_swap_in_mb : int = db . Column ( db . Integer (), nullable = True , default = None )
45- memory_kernel_in_mb : int = db . Column ( db . Integer (), nullable = True , default = None )
46+ memory_in_mb : Mapped [ Optional [ int ]] = mapped_column ( default = None )
47+ memory_swap_in_mb : Mapped [ Optional [ int ]] = mapped_column ( default = None )
48+ memory_kernel_in_mb : Mapped [ Optional [ int ]] = mapped_column ( default = None )
4649
4750
4851class ExerciseEntryService (CommonDbOpsMixin , ModelToStringMixin , db .Model ):
@@ -53,52 +56,47 @@ class ExerciseEntryService(CommonDbOpsMixin, ModelToStringMixin, db.Model):
5356
5457 __to_str_fields__ = ["id" , "exercise_id" ]
5558 __tablename__ = "exercise_entry_service"
56- __allow_unmapped__ = True
5759
58- id = db . Column ( db . Integer , primary_key = True )
60+ id : Mapped [ int ] = mapped_column ( primary_key = True )
5961
6062 # The exercise this entry service belongs to
61- exercise_id : int = db . Column (
62- db . Integer , db . ForeignKey ("exercise.id" , ondelete = "RESTRICT" ), nullable = False
63+ exercise_id : Mapped [ int ] = mapped_column (
64+ ForeignKey ("exercise.id" , ondelete = "RESTRICT" )
6365 )
64- exercise : "Exercise" = db . relationship (
66+ exercise : Mapped [ "Exercise" ] = relationship (
6567 "Exercise" , foreign_keys = [exercise_id ], back_populates = "entry_service"
6668 )
6769
6870 # Path inside the container that is persistet
69- persistance_container_path : str = db . Column ( db . Text (), nullable = True )
71+ persistance_container_path : Mapped [ Optional [ str ]] = mapped_column ( Text )
7072
71- files : List [str ] = db . Column (PickleType (), nullable = True )
73+ files : Mapped [ Optional [ List [str ]]] = mapped_column (PickleType )
7274
7375 # List of commands that are executed when building the service's Docker image.
74- build_cmd : List [str ] = db . Column ( db . PickleType (), nullable = True )
76+ build_cmd : Mapped [ Optional [ List [str ]]] = mapped_column ( PickleType )
7577
76- no_randomize_files : typing .Optional [List [str ]] = db .Column (
77- db .PickleType (), nullable = True
78- )
78+ no_randomize_files : Mapped [Optional [List [str ]]] = mapped_column (PickleType )
7979
80- disable_aslr : bool = db . Column ( db . Boolean (), nullable = False )
80+ disable_aslr : Mapped [ bool ]
8181
8282 # Command that is executed as soon a user connects (list)
83- cmd : List [str ] = db . Column ( db . PickleType (), nullable = False )
83+ cmd : Mapped [ List [str ]] = mapped_column ( PickleType )
8484
85- readonly : bool = db . Column ( db . Boolean (), nullable = False , default = False )
85+ readonly : Mapped [ bool ] = mapped_column ( default = False )
8686
87- allow_internet : bool = db . Column ( db . Boolean (), nullable = False , default = False )
87+ allow_internet : Mapped [ bool ] = mapped_column ( default = False )
8888
8989 # options for the flag that is placed inside the container
90- flag_path : str = db .Column (db .Text (), nullable = True )
91- flag_value : str = db .Column (db .Text (), nullable = True )
92- flag_user : str = db .Column (db .Text (), nullable = True )
93- flag_group : str = db .Column (db .Text (), nullable = True )
94- flag_permission : str = db .Column (db .Text (), nullable = True )
95-
96- ressource_limit_id : int = db .Column (
97- db .Integer ,
98- db .ForeignKey ("exercise_ressource_limits.id" , ondelete = "RESTRICT" ),
99- nullable = True ,
90+ flag_path : Mapped [Optional [str ]] = mapped_column (Text )
91+ flag_value : Mapped [Optional [str ]] = mapped_column (Text )
92+ flag_user : Mapped [Optional [str ]] = mapped_column (Text )
93+ flag_group : Mapped [Optional [str ]] = mapped_column (Text )
94+ flag_permission : Mapped [Optional [str ]] = mapped_column (Text )
95+
96+ ressource_limit_id : Mapped [Optional [int ]] = mapped_column (
97+ ForeignKey ("exercise_ressource_limits.id" , ondelete = "RESTRICT" )
10098 )
101- ressource_limit : RessourceLimits = db . relationship (
99+ ressource_limit : Mapped [ Optional [ RessourceLimits ]] = relationship (
102100 "RessourceLimits" , foreign_keys = [ressource_limit_id ]
103101 )
104102
@@ -127,44 +125,41 @@ class ExerciseService(CommonDbOpsMixin, ModelToStringMixin, db.Model):
127125
128126 __to_str_fields__ = ["id" , "exercise_id" ]
129127 __tablename__ = "exercise_service"
130- __allow_unmapped__ = True
131128
132- id : int = db . Column ( db . Integer , primary_key = True )
129+ id : Mapped [ int ] = mapped_column ( primary_key = True )
133130
134- name : str = db . Column ( db . Text () )
131+ name : Mapped [ Optional [ str ]] = mapped_column ( Text )
135132
136133 # Backref is exercise
137- exercise_id : int = db . Column (
138- db . Integer , db . ForeignKey ("exercise.id" , ondelete = "RESTRICT" ), nullable = False
134+ exercise_id : Mapped [ int ] = mapped_column (
135+ ForeignKey ("exercise.id" , ondelete = "RESTRICT" )
139136 )
140- exercise : "Exercise" = db . relationship (
137+ exercise : Mapped [ "Exercise" ] = relationship (
141138 "Exercise" , foreign_keys = [exercise_id ], back_populates = "services"
142139 )
143140
144- files : List [str ] = db . Column (PickleType (), nullable = True )
145- build_cmd : List [str ] = db . Column ( db . PickleType (), nullable = True )
141+ files : Mapped [ Optional [ List [str ]]] = mapped_column (PickleType )
142+ build_cmd : Mapped [ Optional [ List [str ]]] = mapped_column ( PickleType )
146143
147- disable_aslr : bool = db . Column ( db . Boolean (), nullable = False )
148- cmd : List [str ] = db . Column ( db . PickleType (), nullable = False )
144+ disable_aslr : Mapped [ bool ]
145+ cmd : Mapped [ List [str ]] = mapped_column ( PickleType )
149146
150- readonly : bool = db . Column ( db . Boolean (), nullable = True , default = False )
147+ readonly : Mapped [ Optional [ bool ]] = mapped_column ( default = False )
151148
152- allow_internet : bool = db . Column ( db . Boolean (), nullable = True , default = False )
149+ allow_internet : Mapped [ Optional [ bool ]] = mapped_column ( default = False )
153150
154- instances : List [Instance ] = db . relationship (
151+ instances : Mapped [ List ["InstanceService" ]] = relationship (
155152 "InstanceService" ,
156153 back_populates = "exercise_service" ,
157154 lazy = True ,
158155 passive_deletes = "all" ,
159156 )
160157
161- # health_check_cmd: List[str] = db.Column(db.PickleType(), nullable=False)
162-
163- flag_path : str = db .Column (db .Text (), nullable = True )
164- flag_value : str = db .Column (db .Text (), nullable = True )
165- flag_user : str = db .Column (db .Text (), nullable = True )
166- flag_group : str = db .Column (db .Text (), nullable = True )
167- flag_permission : str = db .Column (db .Text (), nullable = True )
158+ flag_path : Mapped [Optional [str ]] = mapped_column (Text )
159+ flag_value : Mapped [Optional [str ]] = mapped_column (Text )
160+ flag_user : Mapped [Optional [str ]] = mapped_column (Text )
161+ flag_group : Mapped [Optional [str ]] = mapped_column (Text )
162+ flag_permission : Mapped [Optional [str ]] = mapped_column (Text )
168163
169164 @property
170165 def image_name (self ) -> str :
@@ -184,71 +179,66 @@ class Exercise(CommonDbOpsMixin, ModelToStringMixin, db.Model):
184179
185180 __to_str_fields__ = ["id" , "short_name" , "version" , "category" , "build_job_status" ]
186181 __tablename__ = "exercise"
187- __allow_unmapped__ = True
188182
189- id : int = db . Column ( db . Integer , primary_key = True )
183+ id : Mapped [ int ] = mapped_column ( primary_key = True )
190184
191185 # The services that defines the entrypoint of this exercise
192- entry_service : ExerciseEntryService = db . relationship (
186+ entry_service : Mapped [ Optional [ ExerciseEntryService ]] = relationship (
193187 "ExerciseEntryService" ,
194188 uselist = False ,
195189 back_populates = "exercise" ,
196190 passive_deletes = "all" ,
197191 )
198192
199193 # Additional services that are mapped into the network for this exercise.
200- services : List [ExerciseService ] = db . relationship (
194+ services : Mapped [ List [ExerciseService ]] = relationship (
201195 "ExerciseService" , back_populates = "exercise" , lazy = True , passive_deletes = "all"
202196 )
203197
204198 # Folder the template was initially imported from
205- template_import_path : str = db . Column ( db . Text (), nullable = False , unique = False )
199+ template_import_path : Mapped [ str ] = mapped_column ( Text )
206200
207201 # Folder where a copy of the template is stored for persisting it after import
208- template_path : str = db . Column ( db . Text (), nullable = False , unique = True )
202+ template_path : Mapped [ str ] = mapped_column ( Text , unique = True )
209203
210204 # Path to the folder that contains all persisted data of this exercise.
211- persistence_path : str = db . Column ( db . Text (), nullable = False , unique = True )
205+ persistence_path : Mapped [ str ] = mapped_column ( Text , unique = True )
212206
213207 # Name that identifies the exercise
214- short_name : str = db . Column ( db . Text (), nullable = False , unique = False )
208+ short_name : Mapped [ str ] = mapped_column ( Text )
215209
216210 # Version of the exercise used for updating mechanism.
217- version : int = db . Column ( db . Integer (), nullable = False )
211+ version : Mapped [ int ]
218212
219213 # Used to group the exercises
220- category : str = db . Column ( db . Text (), nullable = True , unique = False )
214+ category : Mapped [ Optional [ str ]] = mapped_column ( Text )
221215
222216 # Instances must be submitted before this point in time.
223- submission_deadline_end : datetime .datetime = db . Column ( db . DateTime (), nullable = True )
217+ submission_deadline_end : Mapped [ Optional [ datetime .datetime ]]
224218
225- submission_deadline_start : datetime .datetime = db .Column (
226- db .DateTime (), nullable = True
227- )
219+ submission_deadline_start : Mapped [Optional [datetime .datetime ]]
228220
229- submission_test_enabled : datetime . datetime = db . Column ( db . Boolean (), nullable = False )
221+ submission_test_enabled : Mapped [ bool ]
230222
231223 # Max point a user can get for this exercise. Might be None.
232- max_grading_points : int = db . Column ( db . Integer , nullable = True )
224+ max_grading_points : Mapped [ Optional [ int ]]
233225
234226 # Is this Exercise version deployed by default in case an instance is requested?
235227 # At most one exercise with same short_name can have this flag.
236- is_default : bool = db . Column ( db . Boolean (), nullable = False )
228+ is_default : Mapped [ bool ]
237229
238230 # Log of the last build run
239- build_job_result : str = db . Column ( db . Text (), nullable = True )
231+ build_job_result : Mapped [ Optional [ str ]] = mapped_column ( Text )
240232
241233 # Build status of the docker images that belong to the exercise
242- build_job_status : ExerciseBuildStatus = db .Column (
243- db .Enum (ExerciseBuildStatus ), nullable = False
244- )
234+ build_job_status : Mapped [ExerciseBuildStatus ]
245235
246236 # All running instances of this exercise
247- instances : List [Instance ] = db . relationship (
237+ instances : Mapped [ List [" Instance" ]] = relationship (
248238 "Instance" , back_populates = "exercise" , lazy = True , passive_deletes = "all"
249239 )
250240
251- def get_users_instance (self , user ) -> List [Instance ]:
241+ def get_users_instance (self , user ) -> List [" Instance" ]:
252242 for instance in self .instances :
253243 if instance .user == user :
254244 return instance
@@ -270,7 +260,7 @@ def predecessors(self) -> List[Exercise]:
270260 def is_update (self ) -> bool :
271261 return len (self .predecessors ()) > 0
272262
273- def predecessor (self ) -> Exercise :
263+ def predecessor (self ) -> Optional [ Exercise ] :
274264 predecessors = self .predecessors ()
275265 if predecessors :
276266 return predecessors [0 ]
@@ -298,29 +288,29 @@ def successors(self) -> List[Exercise]:
298288 )
299289 return exercises
300290
301- def successor (self ) -> Exercise :
291+ def successor (self ) -> Optional [ Exercise ] :
302292 successors = self .successors ()
303293 if successors :
304294 return successors [0 ]
305295 else :
306296 return None
307297
308- def head (self ) -> Exercise :
298+ def head (self ) -> Optional [ Exercise ] :
309299 """
310300 Returns the newest version of this exercise.
311301 """
312302 ret = self .successors () + [self ]
313303 return max (ret , key = lambda e : e .version , default = None )
314304
315- def tail (self ) -> Exercise :
305+ def tail (self ) -> Optional [ Exercise ] :
316306 """
317307 Returns the oldest version of this exercise.
318308 """
319309 ret = self .predecessors () + [self ]
320310 return min (ret , key = lambda e : e .version , default = None )
321311
322312 @staticmethod
323- def get_default_exercise (short_name , for_update = False ) -> Exercise :
313+ def get_default_exercise (short_name , for_update = False ) -> Optional [ Exercise ] :
324314 """
325315 Returns and locks the default exercise for the given short_name.
326316 """
@@ -330,7 +320,7 @@ def get_default_exercise(short_name, for_update=False) -> Exercise:
330320 return q .one_or_none ()
331321
332322 @staticmethod
333- def get_exercise (short_name , version , for_update = False ) -> Exercise :
323+ def get_exercise (short_name , version , for_update = False ) -> Optional [ Exercise ] :
334324 exercise = Exercise .query .filter (
335325 and_ (Exercise .short_name == short_name , Exercise .version == version )
336326 )
@@ -354,13 +344,15 @@ def has_started(self) -> bool:
354344 or datetime .datetime .now () > self .submission_deadline_start
355345 )
356346
357- def submission_heads (self ) -> List [Submission ]:
347+ def submission_heads (self ) -> List [" Submission" ]:
358348 """
359349 Returns the most recent submission for this exercise for each user.
360350 Note: This function does not consider Submissions of other
361351 version of this exercise. Hence, the returned submissions might
362352 not be the most recent ones for an specific instance.
363353 """
354+ from .instance import Instance
355+
364356 most_recent_instances = []
365357 instances_per_user = defaultdict (list )
366358 instances = Instance .query .filter (
@@ -374,7 +366,7 @@ def submission_heads(self) -> List[Submission]:
374366 most_recent_instances += [max (instances , key = lambda e : e .creation_ts )]
375367 return [e .submission for e in most_recent_instances if e .submission ]
376368
377- def submission_heads_global (self ) -> List [Submission ]:
369+ def submission_heads_global (self ) -> List [" Submission" ]:
378370 """
379371 Same as .submission_heads(), except only submissions
380372 that have no newer (based on a more recent exercise version)
@@ -400,15 +392,15 @@ def submission_heads_global(self) -> List[Submission]:
400392 return ret
401393
402394 @property
403- def active_instances (self ) -> List [Instance ]:
395+ def active_instances (self ) -> List [" Instance" ]:
404396 """
405397 Get all instances of this exercise that are no submissions.
406398 Note: This function does not returns Instances that belong to
407399 another version of this exercise.
408400 """
409401 return [i for i in self .instances if not i .submission ]
410402
411- def submissions (self , user = None ) -> List [Submission ]:
403+ def submissions (self , user = None ) -> List [" Submission" ]:
412404 """
413405 Get all submissions of this exercise.
414406 Note: This function does not returns Submissions that belong to
@@ -441,7 +433,7 @@ def has_graded_submissions(self) -> bool:
441433 return True
442434 return False
443435
444- def avg_points (self ) -> float :
436+ def avg_points (self ) -> Optional [ float ] :
445437 """
446438 Returns the average points calculated over all submission heads.
447439 If there are no submissions, None is returned.
0 commit comments