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