@@ -531,3 +531,317 @@ def __eq__(self, other):
531531
532532 def __ne__ (self , other ):
533533 return not self .__eq__ (other )
534+
535+
536+ class SaveMixin (object ):
537+ """Mixin for RESTObject's that can be updated."""
538+ def save (self , ** kwargs ):
539+ """Saves the changes made to the object to the server.
540+
541+ Args:
542+ **kwargs: Extra option to send to the server (e.g. sudo)
543+
544+ The object is updated to match what the server returns.
545+ """
546+ updated_data = {}
547+ required , optional = self .manager .get_update_attrs ()
548+ for attr in required :
549+ # Get everything required, no matter if it's been updated
550+ updated_data [attr ] = getattr (self , attr )
551+ # Add the updated attributes
552+ updated_data .update (self ._updated_attrs )
553+
554+ # class the manager
555+ obj_id = self .get_id ()
556+ server_data = self .manager .update (obj_id , updated_data , ** kwargs )
557+ self ._updated_attrs = {}
558+ self ._attrs .update (server_data )
559+
560+
561+ class RESTObject (object ):
562+ """Represents an object built from server data.
563+
564+ It holds the attributes know from te server, and the updated attributes in
565+ another. This allows smart updates, if the object allows it.
566+
567+ You can redefine ``_id_attr`` in child classes to specify which attribute
568+ must be used as uniq ID. None means that the object can be updated without
569+ ID in the url.
570+ """
571+ _id_attr = 'id'
572+
573+ def __init__ (self , manager , attrs ):
574+ self .__dict__ .update ({
575+ 'manager' : manager ,
576+ '_attrs' : attrs ,
577+ '_updated_attrs' : {},
578+ })
579+
580+ def __getattr__ (self , name ):
581+ try :
582+ return self .__dict__ ['_updated_attrs' ][name ]
583+ except KeyError :
584+ try :
585+ return self .__dict__ ['_attrs' ][name ]
586+ except KeyError :
587+ raise AttributeError (name )
588+
589+ def __setattr__ (self , name , value ):
590+ self .__dict__ ['_updated_attrs' ][name ] = value
591+
592+ def __str__ (self ):
593+ data = self ._attrs .copy ()
594+ data .update (self ._updated_attrs )
595+ return '%s => %s' % (type (self ), data )
596+
597+ def __repr__ (self ):
598+ if self ._id_attr :
599+ return '<%s %s:%s>' % (self .__class__ .__name__ ,
600+ self ._id_attr ,
601+ self .get_id ())
602+ else :
603+ return '<%s>' % self .__class__ .__name__
604+
605+ def get_id (self ):
606+ if self ._id_attr is None :
607+ return None
608+ return getattr (self , self ._id_attr )
609+
610+
611+ class RESTObjectList (object ):
612+ """Generator object representing a list of RESTObject's.
613+
614+ This generator uses the Gitlab pagination system to fetch new data when
615+ required.
616+
617+ Note: you should not instanciate such objects, they are returned by calls
618+ to RESTManager.list()
619+
620+ Args:
621+ manager: Manager to attach to the created objects
622+ obj_cls: Type of objects to create from the json data
623+ _list: A GitlabList object
624+ """
625+ def __init__ (self , manager , obj_cls , _list ):
626+ self .manager = manager
627+ self ._obj_cls = obj_cls
628+ self ._list = _list
629+
630+ def __iter__ (self ):
631+ return self
632+
633+ def __len__ (self ):
634+ return len (self ._list )
635+
636+ def __next__ (self ):
637+ return self .next ()
638+
639+ def next (self ):
640+ data = self ._list .next ()
641+ return self ._obj_cls (self .manager , data )
642+
643+
644+ class GetMixin (object ):
645+ def get (self , id , ** kwargs ):
646+ """Retrieve a single object.
647+
648+ Args:
649+ id (int or str): ID of the object to retrieve
650+ **kwargs: Extra data to send to the Gitlab server (e.g. sudo)
651+
652+ Returns:
653+ object: The generated RESTObject.
654+
655+ Raises:
656+ GitlabGetError: If the server cannot perform the request.
657+ """
658+ path = '%s/%s' % (self ._path , id )
659+ server_data = self .gitlab .http_get (path , ** kwargs )
660+ return self ._obj_cls (self , server_data )
661+
662+
663+ class GetWithoutIdMixin (object ):
664+ def get (self , ** kwargs ):
665+ """Retrieve a single object.
666+
667+ Args:
668+ **kwargs: Extra data to send to the Gitlab server (e.g. sudo)
669+
670+ Returns:
671+ object: The generated RESTObject.
672+
673+ Raises:
674+ GitlabGetError: If the server cannot perform the request.
675+ """
676+ server_data = self .gitlab .http_get (self ._path , ** kwargs )
677+ return self ._obj_cls (self , server_data )
678+
679+
680+ class ListMixin (object ):
681+ def list (self , ** kwargs ):
682+ """Retrieves a list of objects.
683+
684+ Args:
685+ **kwargs: Extra data to send to the Gitlab server (e.g. sudo).
686+ If ``all`` is passed and set to True, the entire list of
687+ objects will be returned.
688+
689+ Returns:
690+ RESTObjectList: Generator going through the list of objects, making
691+ queries to the server when required.
692+ If ``all=True`` is passed as argument, returns
693+ list(RESTObjectList).
694+ """
695+
696+ obj = self .gitlab .http_list (self ._path , ** kwargs )
697+ if isinstance (obj , list ):
698+ return [self ._obj_cls (self , item ) for item in obj ]
699+ else :
700+ return RESTObjectList (self , self ._obj_cls , obj )
701+
702+
703+ class GetFromListMixin (ListMixin ):
704+ def get (self , id , ** kwargs ):
705+ """Retrieve a single object.
706+
707+ Args:
708+ id (int or str): ID of the object to retrieve
709+ **kwargs: Extra data to send to the Gitlab server (e.g. sudo)
710+
711+ Returns:
712+ object: The generated RESTObject.
713+
714+ Raises:
715+ GitlabGetError: If the server cannot perform the request.
716+ """
717+ gen = self .list ()
718+ for obj in gen :
719+ if str (obj .get_id ()) == str (id ):
720+ return obj
721+
722+
723+ class RetrieveMixin (ListMixin , GetMixin ):
724+ pass
725+
726+
727+ class CreateMixin (object ):
728+ def _check_missing_attrs (self , data ):
729+ required , optional = self .get_create_attrs ()
730+ missing = []
731+ for attr in required :
732+ if attr not in data :
733+ missing .append (attr )
734+ continue
735+ if missing :
736+ raise AttributeError ("Missing attributes: %s" % ", " .join (missing ))
737+
738+ def get_create_attrs (self ):
739+ """Returns the required and optional arguments.
740+
741+ Returns:
742+ tuple: 2 items: list of required arguments and list of optional
743+ arguments for creation (in that order)
744+ """
745+ if hasattr (self , '_create_attrs' ):
746+ return (self ._create_attrs ['required' ],
747+ self ._create_attrs ['optional' ])
748+ return (tuple (), tuple ())
749+
750+ def create (self , data , ** kwargs ):
751+ """Created a new object.
752+
753+ Args:
754+ data (dict): parameters to send to the server to create the
755+ resource
756+ **kwargs: Extra data to send to the Gitlab server (e.g. sudo)
757+
758+ Returns:
759+ RESTObject: a new instance of the manage object class build with
760+ the data sent by the server
761+ """
762+ self ._check_missing_attrs (data )
763+ if hasattr (self , '_sanitize_data' ):
764+ data = self ._sanitize_data (data , 'create' )
765+ server_data = self .gitlab .http_post (self ._path , post_data = data , ** kwargs )
766+ return self ._obj_cls (self , server_data )
767+
768+
769+ class UpdateMixin (object ):
770+ def _check_missing_attrs (self , data ):
771+ required , optional = self .get_update_attrs ()
772+ missing = []
773+ for attr in required :
774+ if attr not in data :
775+ missing .append (attr )
776+ continue
777+ if missing :
778+ raise AttributeError ("Missing attributes: %s" % ", " .join (missing ))
779+
780+ def get_update_attrs (self ):
781+ """Returns the required and optional arguments.
782+
783+ Returns:
784+ tuple: 2 items: list of required arguments and list of optional
785+ arguments for update (in that order)
786+ """
787+ if hasattr (self , '_update_attrs' ):
788+ return (self ._update_attrs ['required' ],
789+ self ._update_attrs ['optional' ])
790+ return (tuple (), tuple ())
791+
792+ def update (self , id = None , new_data = {}, ** kwargs ):
793+ """Update an object on the server.
794+
795+ Args:
796+ id: ID of the object to update (can be None if not required)
797+ new_data: the update data for the object
798+ **kwargs: Extra data to send to the Gitlab server (e.g. sudo)
799+
800+ Returns:
801+ dict: The new object data (*not* a RESTObject)
802+ """
803+
804+ if id is None :
805+ path = self ._path
806+ else :
807+ path = '%s/%s' % (self ._path , id )
808+
809+ self ._check_missing_attrs (new_data )
810+ if hasattr (self , '_sanitize_data' ):
811+ data = self ._sanitize_data (new_data , 'update' )
812+ server_data = self .gitlab .http_put (self ._path , post_data = data ,
813+ ** kwargs )
814+ return server_data
815+
816+
817+ class DeleteMixin (object ):
818+ def delete (self , id , ** kwargs ):
819+ """Deletes an object on the server.
820+
821+ Args:
822+ id: ID of the object to delete
823+ **kwargs: Extra data to send to the Gitlab server (e.g. sudo)
824+ """
825+ path = '%s/%s' % (self ._path , id )
826+ self .gitlab .http_delete (path , ** kwargs )
827+
828+
829+ class CRUDMixin (GetMixin , ListMixin , CreateMixin , UpdateMixin , DeleteMixin ):
830+ pass
831+
832+
833+ class RESTManager (object ):
834+ """Base class for CRUD operations on objects.
835+
836+ Derivated class must define ``_path`` and ``_obj_cls``.
837+
838+ ``_path``: Base URL path on which requests will be sent (e.g. '/projects')
839+ ``_obj_cls``: The class of objects that will be created
840+ """
841+
842+ _path = None
843+ _obj_cls = None
844+
845+ def __init__ (self , gl , parent_attrs = {}):
846+ self .gitlab = gl
847+ self ._parent_attrs = {} # for nested managers
0 commit comments