1+ # -*- coding: utf-8 -*- 
2+ # This file is part of Invenio. 
3+ # Copyright (C) 2025 CERN. 
4+ # 
5+ # Invenio is free software; you can redistribute it and/or modify it 
6+ # under the terms of the MIT License; see LICENSE file for more details. 
7+ """Higher-level operations for the view handlers and upstream code to use.""" 
8+ 
19from  abc  import  abstractmethod 
210from  contextlib  import  contextmanager 
311from  dataclasses  import  asdict 
1119from  invenio_i18n  import  gettext  as  _ 
1220from  invenio_oauth2server .models  import  Token  as  ProviderToken 
1321from  invenio_oauthclient  import  oauth_link_external_id 
14- from  invenio_oauthclient .models  import  RemoteAccount 
15- from  sqlalchemy  import  delete , select 
22+ from  sqlalchemy  import  delete 
1623from  sqlalchemy .exc  import  NoResultFound 
1724from  werkzeug .utils  import  cached_property 
1825
4350
4451
4552class  VCSService :
53+     """ 
54+     High level glue operations that operate on both the VCS and the DB. 
55+ 
56+     Because provider instances are user-specific, this class is too. 
57+     """ 
58+ 
4659    def  __init__ (self , provider : "RepositoryServiceProvider" ) ->  None :
60+         """Please construct the service using the `for_provider_and_user` method instead.""" 
4761        self .provider  =  provider 
4862
4963    @staticmethod  
5064    def  for_provider_and_user (provider_id : str , user_id : int ):
65+         """Construct VCSService for a locally configured provider and a user with a DB-queried access token.""" 
5166        return  VCSService (get_provider_by_id (provider_id ).for_user (user_id ))
5267
5368    @staticmethod  
5469    def  for_provider_and_token (provider_id : str , user_id : int , access_token : str ):
70+         """Construct VCSService for a locally configured provider and a user with a predefined access token.""" 
5571        return  VCSService (
5672            get_provider_by_id (provider_id ).for_access_token (user_id , access_token )
5773        )
5874
5975    @cached_property  
6076    def  is_authenticated (self ):
77+         """Whether we have a valid VCS API token for the user. Should (almost) always return True.""" 
6178        return  self .provider .session_token  is  not   None 
6279
6380    @property  
@@ -102,7 +119,7 @@ def get_repo_latest_release(self, repo):
102119        return  current_vcs .release_api_class (release_object , self .provider )
103120
104121    def  list_repo_releases (self , repo ):
105-         #  Retrieve releases and sort them by creation date
122+         """ Retrieve releases and sort them by creation date.""" 
106123        release_instances  =  []
107124        for  release_object  in  repo .releases .order_by (Release .created ):
108125            release_instances .append (
@@ -111,6 +128,7 @@ def list_repo_releases(self, repo):
111128        return  release_instances 
112129
113130    def  get_repo_default_branch (self , repo_id ):
131+         """Return the locally-synced default branch.""" 
114132        db_repo  =  self .user_available_repositories .filter (
115133            Repository .provider_id  ==  repo_id 
116134        ).first ()
@@ -121,7 +139,7 @@ def get_repo_default_branch(self, repo_id):
121139        return  db_repo .default_branch 
122140
123141    def  get_last_sync_time (self ):
124-         """Retrieves the last sync delta time from github 's client extra data. 
142+         """Retrieves the last sync delta time from VCS 's client extra data. 
125143
126144        Time is computed as the delta between now and the last sync time. 
127145        """ 
@@ -156,7 +174,7 @@ def check_repo_access_permissions(self, repo: Repository):
156174        Repo has access if any of the following is True: 
157175
158176        - user is the owner of the repo 
159-         - user has access to the repo in GitHub (stored in RemoteAccount.extra_data.repos)  
177+         - user has access to the repo in the VCS  
160178        """ 
161179        if  self .provider .user_id  and  repo :
162180            user_is_collaborator  =  any (
@@ -179,6 +197,7 @@ def check_repo_access_permissions(self, repo: Repository):
179197    def  sync_repo_users (self , db_repo : Repository ):
180198        """ 
181199        Synchronises the member users of the repository. 
200+ 
182201        This retrieves a list of the IDs of users from the VCS who have sufficient access to the 
183202        repository (i.e. being able to access all details and create/manage webhooks). 
184203        The user IDs are compared locally to find Invenio users who have connected their VCS account. 
@@ -187,7 +206,6 @@ def sync_repo_users(self, db_repo: Repository):
187206
188207        :return: boolean of whether any changed were made to the DB 
189208        """ 
190- 
191209        vcs_user_ids  =  self .provider .list_repository_user_ids (db_repo .provider_id )
192210        if  vcs_user_ids  is  None :
193211            return 
@@ -235,8 +253,8 @@ def sync(self, hooks=True, async_hooks=True):
235253
236254        .. note:: 
237255
238-             Syncing happens from GitHub's  direction only. This means that we 
239-             consider the information on GitHub  as valid, and we overwrite our 
256+             Syncing happens from the VCS'  direction only. This means that we 
257+             consider the information on VCS  as valid, and we overwrite our 
240258            own state based on this information. 
241259        """ 
242260        vcs_repos  =  self .provider .list_repositories ()
@@ -330,13 +348,13 @@ def _sync_hooks(self, repo_ids, asynchronous=True):
330348            )
331349
332350    def  sync_repo_hook (self , repo_id ):
333-         """Sync a GitHub  repo's hook with the locally stored repo.""" 
351+         """Sync a VCS  repo's hook with the locally stored repo.""" 
334352        # Get the hook that we may have set in the past 
335353        hook  =  self .provider .get_first_valid_webhook (repo_id )
336354        vcs_repo  =  self .provider .get_repository (repo_id )
337355        assert  vcs_repo  is  not   None 
338356
339-         # If hook on GitHub  exists, get or create corresponding db object and 
357+         # If hook on the VCS  exists, get or create corresponding db object and 
340358        # enable the hook. Otherwise remove the old hook information. 
341359        db_repo  =  Repository .get (self .provider .factory .id , provider_id = repo_id )
342360
@@ -359,17 +377,17 @@ def sync_repo_hook(self, repo_id):
359377                self .mark_repo_disabled (db_repo )
360378
361379    def  mark_repo_disabled (self , db_repo : Repository ):
362-         """Disables an user  repository.""" 
380+         """Marks a  repository as disabled .""" 
363381        db_repo .hook  =  None 
364382        db_repo .enabled_by_id  =  None 
365383
366384    def  mark_repo_enabled (self , db_repo : Repository , hook_id : str ):
367-         """Enables an user  repository.""" 
385+         """Marks a  repository as enabled .""" 
368386        db_repo .hook  =  hook_id 
369387        db_repo .enabled_by_id  =  self .provider .user_id 
370388
371389    def  init_account (self ):
372-         """Setup a new GitHub  account.""" 
390+         """Setup a new VCS  account.""" 
373391        if  not  self .provider .remote_account :
374392            raise  RemoteAccountNotFound (
375393                self .provider .user_id , _ ("Remote account was not found for user." )
@@ -405,6 +423,7 @@ def init_account(self):
405423        db .session .add (self .provider .remote_account )
406424
407425    def  enable_repository (self , repository_id ):
426+         """Creates the hook for a repository and marks it as enabled.""" 
408427        db_repo  =  self .user_available_repositories .filter (
409428            Repository .provider_id  ==  repository_id 
410429        ).first ()
@@ -421,6 +440,7 @@ def enable_repository(self, repository_id):
421440        return  True 
422441
423442    def  disable_repository (self , repository_id , hook_id = None ):
443+         """Deletes the hook for a repository and marks it as disabled.""" 
424444        db_repo  =  self .user_available_repositories .filter (
425445            Repository .provider_id  ==  repository_id 
426446        ).first ()
@@ -441,7 +461,14 @@ def disable_repository(self, repository_id, hook_id=None):
441461
442462
443463class  VCSRelease :
444-     """A GitHub release.""" 
464+     """ 
465+     Represents a release and common high-level operations that can be performed on it. 
466+ 
467+     This class is often overriden upstream (e.g. in `invenio-rdm-records`) to specify 
468+     what a 'publish' event should do on a given Invenio implementation. 
469+     This module does not attempt to publish a record or anything similar, as `invenio-vcs` 
470+     is designed to work on any Invenio instance (not just RDM). 
471+     """ 
445472
446473    def  __init__ (self , release : Release , provider : "RepositoryServiceProvider" ):
447474        """Constructor.""" 
@@ -466,6 +493,7 @@ def payload(self):
466493
467494    @cached_property  
468495    def  _generic_release_and_repo (self ):
496+         """Converts the VCS-specific payload into a tuple of (GenericRelease, GenericRepository).""" 
469497        return  self .provider .factory .webhook_event_to_generic (self .payload )
470498
471499    @cached_property  
@@ -514,10 +542,9 @@ def user_identity(self):
514542    def  contributors (self ):
515543        """Get list of contributors to a repository. 
516544
517-         The list of contributors is fetched from Github API , filtered for type "User" and sorted by contributions. 
545+         The list of contributors is fetched from the VCS , filtered for type "User" and sorted by contributions. 
518546
519547        :returns: a generator of objects that contains contributors information. 
520-         :raises UnexpectedGithubResponse: when Github API returns a status code other than 200. 
521548        """ 
522549        max_contributors  =  current_app .config .get ("VCS_MAX_CONTRIBUTORS_NUMBER" , 30 )
523550        return  self .provider .list_repository_contributors (
@@ -581,17 +608,17 @@ def release_published(self):
581608
582609    @contextmanager  
583610    def  fetch_zipball_file (self ):
584-         """Fetch release zipball file using the current github  session.""" 
611+         """Fetch release zipball file using the current VCS  session.""" 
585612        timeout  =  current_app .config .get ("VCS_ZIPBALL_TIMEOUT" , 300 )
586613        zipball_url  =  self .resolve_zipball_url ()
587614        return  self .provider .fetch_release_zipball (zipball_url , timeout )
588615
589616    def  publish (self ):
590-         """Publish a GitHub  release.""" 
617+         """Publish a VCS  release.""" 
591618        raise  NotImplementedError 
592619
593620    def  process_release (self ):
594-         """Processes a github  release.""" 
621+         """Processes a VCS  release.""" 
595622        raise  NotImplementedError 
596623
597624    def  resolve_record (self ):
@@ -616,5 +643,5 @@ def badge_value(self):
616643
617644    @property  
618645    def  record_url (self ):
619-         """Release self url (e.g. github  HTML url).""" 
646+         """Release self url (e.g. VCS  HTML url).""" 
620647        raise  NotImplementedError 
0 commit comments