From a74110be1201c1c003cebf3c1751e77171c82dc9 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Tue, 21 Oct 2025 12:34:02 +0200 Subject: [PATCH 01/26] Experimenting --- pyproject.toml | 1 + travo/gitlab.py | 39 ++++++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2e6888b..797edde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ "typing_utils", "pytest", "requests", + "requests-oauthlib", "tqdm", "anybadge", "i18nice[YAML]", diff --git a/travo/gitlab.py b/travo/gitlab.py index b5f5e05..1579e0f 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -33,6 +33,8 @@ from typing import ( ) import zipfile +from oauthlib.oauth2 import BackendApplicationClient +from requests_oauthlib import OAuth2Session from .i18n import _ from .utils import urlencode, run, getLogger @@ -182,6 +184,7 @@ class GitLab: - If this session is already authenticated, do nothing - If a token file exists, load the token from there, and set it from now on. + - If not token exists check if a PRIVATE_TOKEN is set - Otherwise request a new token through login and password authentication, and set it. Unless the credentials are passed as arguments, they are requested interactively by the @@ -285,13 +288,35 @@ class GitLab: self._current_user = anonymous_user return - result = self.session.post( - self.base_url + "/oauth/token", - params=dict( - grant_type="password", username=username, password=password, scope="api" - ), - ) - token = result.json().get("access_token") + # Try to retrieve a PRIVATE_TOKEN from environment variable + if "GITLAB_PRIVATE_TOKEN" in os.environ: + gitlab_private_token = os.environ["GITLAB_PRIVATE_TOKEN"] + #client = BackendApplicationClient(client_id=username) + #oauth = OAuth2Session(client=client) + #token = oauth.fetch_token( + # token_url=self.base_url + '/oauth2/token', + # include_client_id=True, + # client_id=username, + # private-token=gitlab_private_token, + # scope="api", + # headers={"PRIVATE-TOKEN": gitlab_private_token} + #) + result = self.session.post( + self.base_url + "/oauth2/token", + params={ + "private-token": gitlab_private_token, "scope": "api" + }, + ) + print(result) + token = result.json().get("access_token") + else: + result = self.session.post( + self.base_url + "/oauth/token", + params=dict( + grant_type="password", username=username, password=password, scope="api" + ), + ) + token = result.json().get("access_token") # TODO: handle connection failures if token is None: # TODO: pourrait réessayer -- GitLab From fc593e3ec18764e7236f3deaa5b294c8f9c87d24 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 22 Oct 2025 16:03:21 +0200 Subject: [PATCH 02/26] experimenting --- travo/gitlab.py | 59 ++++++++++++++++++++----------------------------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/travo/gitlab.py b/travo/gitlab.py index 1579e0f..730a796 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -33,8 +33,6 @@ from typing import ( ) import zipfile -from oauthlib.oauth2 import BackendApplicationClient -from requests_oauthlib import OAuth2Session from .i18n import _ from .utils import urlencode, run, getLogger @@ -73,6 +71,7 @@ class GitLab: debug: bool = False home_dir: str = str(pathlib.Path.home()) token: Optional[str] = None + token_type: Optional[Union["JWT", "PERSONAL_ACCESS"]] = None token_expires_at: Optional[float] = None _current_user: Optional[Union["User", "AnonymousUser"]] = None base_url: str @@ -90,6 +89,7 @@ class GitLab: self, base_url: str, token: Optional[str] = None, + token_type: Optional[Union["JWT", "PERSONAL_ACCESS"]] = "JWT", log: logging.Logger = getLogger(), home_dir: Optional[str] = None, ): @@ -125,24 +125,28 @@ class GitLab: @return whether the token is valid """ - t = time.time() - response = self.session.get( - self.base_url + "/oauth/token/info", data=dict(access_token=token) - ) - try: - json = response.json() - except requests.HTTPError: - response.raise_for_status() - if "error" in json: - assert json["error"] == "invalid_token" - self.log.info( - _("invalid token", error_description=json["error_description"]) + if self.token_type == "JWT": + t = time.time() + response = self.session.get( + self.base_url + "/oauth/token/info", data=dict(access_token=token) ) - return False + try: + json = response.json() + except requests.HTTPError: + response.raise_for_status() + if "error" in json: + assert json["error"] == "invalid_token" + self.log.info( + _("invalid token", error_description=json["error_description"]) + ) + return False - self.session.headers.update({"Authorization": f"Bearer {token}"}) + self.session.headers.update({"Authorization": f"Bearer {token}"}) + self.token_expires_at = t + json["expires_in"] + elif self.token_type == "PERSONAL_ACCESS": + self.session.headers.update({"PRIVATE-TOKEN": f"{token}"}) + self.token = token - self.token_expires_at = t + json["expires_in"] if not nosave: os.makedirs(os.path.dirname(self.token_file()), exist_ok=True) with os.fdopen( @@ -163,6 +167,8 @@ class GitLab: del self.session.headers["Authorization"] if "TRAVO_TOKEN" in os.environ: del os.environ["TRAVO_TOKEN"] + if "GITLAB_PRIVATE_TOKEN" in os.environ: + del os.environ["GITLAB_PRIVATE_TOKEN"] token_file = self.token_file() # Testing whether the file exists before removing it is not # atomic; so just try to remove it. @@ -291,24 +297,7 @@ class GitLab: # Try to retrieve a PRIVATE_TOKEN from environment variable if "GITLAB_PRIVATE_TOKEN" in os.environ: gitlab_private_token = os.environ["GITLAB_PRIVATE_TOKEN"] - #client = BackendApplicationClient(client_id=username) - #oauth = OAuth2Session(client=client) - #token = oauth.fetch_token( - # token_url=self.base_url + '/oauth2/token', - # include_client_id=True, - # client_id=username, - # private-token=gitlab_private_token, - # scope="api", - # headers={"PRIVATE-TOKEN": gitlab_private_token} - #) - result = self.session.post( - self.base_url + "/oauth2/token", - params={ - "private-token": gitlab_private_token, "scope": "api" - }, - ) - print(result) - token = result.json().get("access_token") + token = gitlab_private_token else: result = self.session.post( self.base_url + "/oauth/token", -- GitLab From e3f7a02af56aa5376956efd33b0346a1d083c76b Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Mon, 3 Nov 2025 11:22:58 +0100 Subject: [PATCH 03/26] Add private access token interactively --- travo/gitlab.py | 139 ++++++++++++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 51 deletions(-) diff --git a/travo/gitlab.py b/travo/gitlab.py index 730a796..b2bb654 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -99,6 +99,7 @@ class GitLab: self.api = base_url + "api/v4/" self.session = requests.Session() self.token = None + self.token_type = token_type self.log = log self.on_missing_credentials = request_credentials_basic if home_dir is not None: @@ -190,11 +191,13 @@ class GitLab: - If this session is already authenticated, do nothing - If a token file exists, load the token from there, and set it from now on. - - If not token exists check if a PRIVATE_TOKEN is set - - Otherwise request a new token through login and password - authentication, and set it. Unless the credentials are - passed as arguments, they are requested interactively by the - user. The token is stored in a token file for future reuse. + - Otherwise if the `token_type` is set to "JWT" request a new token + through login and password authentication, and set it. + - if the `token_type` is set to "PERSONAL_ACCESS" request to input + the personal access token. + - Unless the credentials are passed as arguments, they are requested + interactively by the user. + The token is stored in a token file for future reuse. In case of failure, for example, because no credentials are provided and `self.interactive` is `False`, an @@ -255,50 +258,46 @@ class GitLab: {'emoji': None, 'message': None, 'availability': None, 'message_html': ''... """ - if self.token is not None: - assert self.token_expires_at is not None - if time.time() < self.token_expires_at - 60: - # Assumption: the token is valid; unless the token has - # been revoked, this should be correct. - return - # The token has expired or is about to expire in less than - # one minute. Clear it. We assume that the token in the - # persistent cache or in TRAVO_TOKEN, if present, are the - # same. This is correct unless there is a concurrent travo - # process. So clear then as well. - self.logout() - - # Try to retrieve token from environment variable - if "TRAVO_TOKEN" in os.environ: - if self.set_token(os.environ["TRAVO_TOKEN"], nosave=True): - return - raise AuthenticationError( - _("invalid token environment variable", url=self.base_url) - ) - - # Try to retrieve token from persistent cache - token_file = self.token_file() - if os.path.exists(token_file): - if self.set_token(io.open(token_file).read().rstrip()): - return - self.logout() + if self.token_type == "JWT": + if self.token is not None: + assert self.token_expires_at is not None + if time.time() < self.token_expires_at - 60: + # Assumption: the token is valid; unless the token has + # been revoked, this should be correct. + return + # The token has expired or is about to expire in less than + # one minute. Clear it. We assume that the token in the + # persistent cache or in TRAVO_TOKEN, if present, are the + # same. This is correct unless there is a concurrent travo + # process. So clear then as well. + self.logout() + + # Try to retrieve token from environment variable + if "TRAVO_TOKEN" in os.environ: + if self.set_token(os.environ["TRAVO_TOKEN"], nosave=True): + return + raise AuthenticationError( + _("invalid token environment variable", url=self.base_url) + ) - # No token available - if username is None: - if self._current_user is anonymous_user and anonymous_ok: + # Try to retrieve token from persistent cache + token_file = self.token_file() + if os.path.exists(token_file): + if self.set_token(io.open(token_file).read().rstrip()): + return + self.logout() + + # No token available + if username is None: + if self._current_user is anonymous_user and anonymous_ok: + return + username, password = self.on_missing_credentials( + forge=self, username=username, password=password + ) + if username is not None and username == "anonymous" and anonymous_ok: + self._current_user = anonymous_user return - username, password = self.on_missing_credentials( - forge=self, username=username, password=password - ) - if username is not None and username == "anonymous" and anonymous_ok: - self._current_user = anonymous_user - return - # Try to retrieve a PRIVATE_TOKEN from environment variable - if "GITLAB_PRIVATE_TOKEN" in os.environ: - gitlab_private_token = os.environ["GITLAB_PRIVATE_TOKEN"] - token = gitlab_private_token - else: result = self.session.post( self.base_url + "/oauth/token", params=dict( @@ -306,11 +305,49 @@ class GitLab: ), ) token = result.json().get("access_token") - # TODO: handle connection failures - if token is None: - # TODO: pourrait réessayer - raise AuthenticationError(_("invalid credentials", url=self.base_url)) - assert self.set_token(token) + # TODO: handle connection failures + if token is None: + # TODO: pourrait réessayer + raise AuthenticationError(_("invalid credentials", url=self.base_url)) + assert self.set_token(token) + + elif self.token_type == "PERSONAL_ACCESS": + #if self.token is not None: + # assert self.token_expires_at is not None + # if time.time() < self.token_expires_at - 60: + # Assumption: the token is valid; unless the token has + # been revoked, this should be correct. + # return + # The token has expired or is about to expire in less than + # one minute. Clear it. We assume that the token in the + # persistent cache or in TRAVO_TOKEN, if present, are the + # same. This is correct unless there is a concurrent travo + # process. So clear then as well. + # self.logout() + + # Try to retrieve token from environment variable + if "GITLAB_PRIVATE_TOKEN" in os.environ: + if self.set_token(os.environ["GITLAB_PRIVATE_TOKEN"], nosave=False): + return + raise AuthenticationError( + _("invalid token environment variable", url=self.base_url) + ) + + # Try to retrieve token from persistent cache + token_file = self.token_file() + if os.path.exists(token_file): + if self.set_token(io.open(token_file).read().rstrip()): + return + self.logout() + + # No token available + print('Enter your personal access token:') + token = input() + if token is None: + # TODO: pourrait réessayer + raise AuthenticationError(_("invalid credentials", url=self.base_url)) + assert self.set_token(token) + def get(self, path: str, data: dict = {}) -> requests.Response: """Issue a GET request to the forge's API""" -- GitLab From 6ba5fd7d25208859c8c4b502b6863ef6abbe5401 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Mon, 3 Nov 2025 13:05:36 +0100 Subject: [PATCH 04/26] Fix header when logging out, fix type definition --- travo/gitlab.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/travo/gitlab.py b/travo/gitlab.py index b2bb654..28cb151 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -41,6 +41,8 @@ R = TypeVar("R", "Group", "Project", "Namespace", "User") # Job = TypeAlias(JSON) # Python 3.10 JSON = Any # Could be made more specific Job = JSON # Could be made more specific +JWT = "JWT" # Json Web Token type +PERSONAL_ACCESS = "PERSONAL_ACCESS" # Personal Access Token type class ResourceNotFoundError(RuntimeError): @@ -71,7 +73,7 @@ class GitLab: debug: bool = False home_dir: str = str(pathlib.Path.home()) token: Optional[str] = None - token_type: Optional[Union["JWT", "PERSONAL_ACCESS"]] = None + token_type: Optional[Union[JWT, PERSONAL_ACCESS]] = None token_expires_at: Optional[float] = None _current_user: Optional[Union["User", "AnonymousUser"]] = None base_url: str @@ -89,7 +91,7 @@ class GitLab: self, base_url: str, token: Optional[str] = None, - token_type: Optional[Union["JWT", "PERSONAL_ACCESS"]] = "JWT", + token_type: Optional[Union[JWT, PERSONAL_ACCESS]] = JWT, log: logging.Logger = getLogger(), home_dir: Optional[str] = None, ): @@ -126,7 +128,7 @@ class GitLab: @return whether the token is valid """ - if self.token_type == "JWT": + if self.token_type == JWT: t = time.time() response = self.session.get( self.base_url + "/oauth/token/info", data=dict(access_token=token) @@ -144,9 +146,9 @@ class GitLab: self.session.headers.update({"Authorization": f"Bearer {token}"}) self.token_expires_at = t + json["expires_in"] - elif self.token_type == "PERSONAL_ACCESS": + elif self.token_type == PERSONAL_ACCESS: self.session.headers.update({"PRIVATE-TOKEN": f"{token}"}) - + self.token = token if not nosave: os.makedirs(os.path.dirname(self.token_file()), exist_ok=True) @@ -168,6 +170,8 @@ class GitLab: del self.session.headers["Authorization"] if "TRAVO_TOKEN" in os.environ: del os.environ["TRAVO_TOKEN"] + if "PRIVATE_TOKEN" in self.session.headers: + del self.session.headers["PRIVATE-TOKEN"] if "GITLAB_PRIVATE_TOKEN" in os.environ: del os.environ["GITLAB_PRIVATE_TOKEN"] token_file = self.token_file() @@ -258,7 +262,7 @@ class GitLab: {'emoji': None, 'message': None, 'availability': None, 'message_html': ''... """ - if self.token_type == "JWT": + if self.token_type == JWT: if self.token is not None: assert self.token_expires_at is not None if time.time() < self.token_expires_at - 60: @@ -301,7 +305,10 @@ class GitLab: result = self.session.post( self.base_url + "/oauth/token", params=dict( - grant_type="password", username=username, password=password, scope="api" + grant_type="password", + username=username, + password=password, + scope="api", ), ) token = result.json().get("access_token") @@ -311,18 +318,18 @@ class GitLab: raise AuthenticationError(_("invalid credentials", url=self.base_url)) assert self.set_token(token) - elif self.token_type == "PERSONAL_ACCESS": - #if self.token is not None: + elif self.token_type == PERSONAL_ACCESS: + # if self.token is not None: # assert self.token_expires_at is not None # if time.time() < self.token_expires_at - 60: - # Assumption: the token is valid; unless the token has - # been revoked, this should be correct. + # Assumption: the token is valid; unless the token has + # been revoked, this should be correct. # return - # The token has expired or is about to expire in less than - # one minute. Clear it. We assume that the token in the - # persistent cache or in TRAVO_TOKEN, if present, are the - # same. This is correct unless there is a concurrent travo - # process. So clear then as well. + # The token has expired or is about to expire in less than + # one minute. Clear it. We assume that the token in the + # persistent cache or in TRAVO_TOKEN, if present, are the + # same. This is correct unless there is a concurrent travo + # process. So clear then as well. # self.logout() # Try to retrieve token from environment variable @@ -341,14 +348,13 @@ class GitLab: self.logout() # No token available - print('Enter your personal access token:') + print("Enter your personal access token:") token = input() if token is None: # TODO: pourrait réessayer raise AuthenticationError(_("invalid credentials", url=self.base_url)) assert self.set_token(token) - def get(self, path: str, data: dict = {}) -> requests.Response: """Issue a GET request to the forge's API""" url = self.api + path -- GitLab From 7cf62876b387fbd2eaa1f5ab308dc79f6360c3e9 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Mon, 3 Nov 2025 13:56:14 +0100 Subject: [PATCH 05/26] Use personal access token for git operations. --- travo/gitlab.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/travo/gitlab.py b/travo/gitlab.py index 28cb151..046c4c5 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -901,7 +901,10 @@ class GitLab: if not anonymous: self.login(anonymous_ok=anonymous_ok) if self.get_current_user() is not anonymous_user: - env["TRAVO_TOKEN"] = cast(str, self.token) + if self.token_type == JWT: + env["TRAVO_TOKEN"] = cast(str, self.token) + elif self.token_type == PERSONAL_ACCESS: + env["GITLAB_PRIVATE_TOKEN"] = cast(str, self.token) env["GIT_ASKPASS"] = "travo-echo-travo-token" git = ["git", "-c", "credential.username=oauth2"] return run(git + list(args), env=env, **kargs) -- GitLab From b939ab606d3b81d8589c3c662b496d96ff8e8250 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 5 Nov 2025 11:05:47 +0100 Subject: [PATCH 06/26] Fix mypy --- travo/gitlab.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/travo/gitlab.py b/travo/gitlab.py index 046c4c5..cf60a30 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -41,8 +41,8 @@ R = TypeVar("R", "Group", "Project", "Namespace", "User") # Job = TypeAlias(JSON) # Python 3.10 JSON = Any # Could be made more specific Job = JSON # Could be made more specific -JWT = "JWT" # Json Web Token type -PERSONAL_ACCESS = "PERSONAL_ACCESS" # Personal Access Token type +JWT: str = "JWT" # Json Web Token type +PERSONAL_ACCESS: str = "PERSONAL_ACCESS" # Personal Access Token type class ResourceNotFoundError(RuntimeError): @@ -73,7 +73,7 @@ class GitLab: debug: bool = False home_dir: str = str(pathlib.Path.home()) token: Optional[str] = None - token_type: Optional[Union[JWT, PERSONAL_ACCESS]] = None + token_type: Optional[str] = None token_expires_at: Optional[float] = None _current_user: Optional[Union["User", "AnonymousUser"]] = None base_url: str @@ -91,7 +91,7 @@ class GitLab: self, base_url: str, token: Optional[str] = None, - token_type: Optional[Union[JWT, PERSONAL_ACCESS]] = JWT, + token_type: Optional[str] = JWT, log: logging.Logger = getLogger(), home_dir: Optional[str] = None, ): -- GitLab From 72480be88d1b8ce07a41f87ba3dfad623d192aed Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 5 Nov 2025 11:36:50 +0100 Subject: [PATCH 07/26] Use the personal access token as auth for git operations --- travo/console_scripts.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/travo/console_scripts.py b/travo/console_scripts.py index 1ac4bee..d76c00f 100755 --- a/travo/console_scripts.py +++ b/travo/console_scripts.py @@ -354,4 +354,7 @@ def travo_echo_travo_token() -> None: This script is used as GIT_ASKPASS callback to provide the gitlab authentication token to git """ - print(os.environ["TRAVO_TOKEN"]) + try: + print(os.environ["TRAVO_TOKEN"]) + except KeyError: + print(os.environ["GITLAB_PRIVATE_TOKEN"]) -- GitLab From c6a20ea1a12d708914c4ce640331ffe733600b75 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 5 Nov 2025 16:29:55 +0100 Subject: [PATCH 08/26] Remove unuseful dependency, blur input token --- pyproject.toml | 1 - travo/gitlab.py | 3 +-- travo/locale/travo.en.yml | 1 + travo/locale/travo.fr.yml | 1 + 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 797edde..2e6888b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,6 @@ dependencies = [ "typing_utils", "pytest", "requests", - "requests-oauthlib", "tqdm", "anybadge", "i18nice[YAML]", diff --git a/travo/gitlab.py b/travo/gitlab.py index cf60a30..11f0019 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -348,8 +348,7 @@ class GitLab: self.logout() # No token available - print("Enter your personal access token:") - token = input() + token = getpass.getpass(_("personal access token") + ": ") if token is None: # TODO: pourrait réessayer raise AuthenticationError(_("invalid credentials", url=self.base_url)) diff --git a/travo/locale/travo.en.yml b/travo/locale/travo.en.yml index e702f06..5f14b57 100644 --- a/travo/locale/travo.en.yml +++ b/travo/locale/travo.en.yml @@ -128,3 +128,4 @@ en: try to merge: "Trying to merge" no content: "No %{content}" corrupted assignment directory: "Corrupted assignment directory" + personal access token: "Enter your personal access token" diff --git a/travo/locale/travo.fr.yml b/travo/locale/travo.fr.yml index 04a09f7..19e4da7 100644 --- a/travo/locale/travo.fr.yml +++ b/travo/locale/travo.fr.yml @@ -128,3 +128,4 @@ fr: initializing submission: "Initialisation de la soumission à partir du devoir" your submission: "Votre submission" corrupted assignment directory: "Corrupted assignment directory" + personal access token: "Tapez votre jeton d'accès personnel" -- GitLab From a973b5dc97036beff9e99b6ad8d35054cc24ca84 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Thu, 6 Nov 2025 16:07:52 +0100 Subject: [PATCH 09/26] Working on the tests --- travo/tests/test_console_scripts.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/travo/tests/test_console_scripts.py b/travo/tests/test_console_scripts.py index c13a8e8..6e0ffb5 100644 --- a/travo/tests/test_console_scripts.py +++ b/travo/tests/test_console_scripts.py @@ -1,6 +1,6 @@ import pytest import os -from travo.console_scripts import Travo +from travo.console_scripts import Travo, travo_echo_travo_token @pytest.mark.parametrize("embed_option", [False, True]) @@ -14,3 +14,14 @@ def test_quickstart(embed_option, tmp_path): clab_dir = course_dir assert os.path.isdir(course_dir + "/Instructors") assert os.path.isdir(clab_dir + "/ComputerLab") + + +@pytest.mark.parametrize("token", ["GITLAB_PRIVATE_TOKEN", "TRAVO_TOKEN"]) +def test_echo_token(capsys, token): + personal_token = "personal_token" + os.environ[token] = personal_token + travo_echo_travo_token() + captured = capsys.readouterr().out.rstrip() + assert captured == personal_token + + del os.environ[token] -- GitLab From 2aa9e6620b809b3d5a3bab592b686377cc6a9aac Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Thu, 6 Nov 2025 17:18:26 +0100 Subject: [PATCH 10/26] Configure coverage --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 797edde..3695540 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,6 +180,9 @@ docstring-code-format = false # enabled. docstring-code-line-length = "dynamic" +[tool.coverage.run] +omit = travo/tests/* + [tool.tox] legacy_tox_ini = """ [tox] -- GitLab From fbd9d40163977505d0bf05b67f565ffd9abd7bc3 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Thu, 6 Nov 2025 17:26:15 +0100 Subject: [PATCH 11/26] Fix pyproject --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1c067fa..4461114 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,7 +180,7 @@ docstring-code-format = false docstring-code-line-length = "dynamic" [tool.coverage.run] -omit = travo/tests/* +omit = "travo/tests/*" [tool.tox] legacy_tox_ini = """ -- GitLab From e881a31fa4d9fd18038549288511634a301d8501 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Thu, 6 Nov 2025 17:49:52 +0100 Subject: [PATCH 12/26] Fix pyproject --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4461114..a4f5efb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,7 +180,7 @@ docstring-code-format = false docstring-code-line-length = "dynamic" [tool.coverage.run] -omit = "travo/tests/*" +omit = ["travo/tests/*"] [tool.tox] legacy_tox_ini = """ -- GitLab From 74808479c264581ab03ea3eaefa9e1efab31cdd5 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Mon, 10 Nov 2025 14:07:51 +0100 Subject: [PATCH 13/26] Simplify environment variables --- travo/console_scripts.py | 5 +- travo/gitlab.py | 92 +++++++++-------------------- travo/tests/test_console_scripts.py | 7 +-- 3 files changed, 33 insertions(+), 71 deletions(-) diff --git a/travo/console_scripts.py b/travo/console_scripts.py index d76c00f..1ac4bee 100755 --- a/travo/console_scripts.py +++ b/travo/console_scripts.py @@ -354,7 +354,4 @@ def travo_echo_travo_token() -> None: This script is used as GIT_ASKPASS callback to provide the gitlab authentication token to git """ - try: - print(os.environ["TRAVO_TOKEN"]) - except KeyError: - print(os.environ["GITLAB_PRIVATE_TOKEN"]) + print(os.environ["TRAVO_TOKEN"]) diff --git a/travo/gitlab.py b/travo/gitlab.py index 11f0019..1d30509 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -172,8 +172,6 @@ class GitLab: del os.environ["TRAVO_TOKEN"] if "PRIVATE_TOKEN" in self.session.headers: del self.session.headers["PRIVATE-TOKEN"] - if "GITLAB_PRIVATE_TOKEN" in os.environ: - del os.environ["GITLAB_PRIVATE_TOKEN"] token_file = self.token_file() # Testing whether the file exists before removing it is not # atomic; so just try to remove it. @@ -262,36 +260,36 @@ class GitLab: {'emoji': None, 'message': None, 'availability': None, 'message_html': ''... """ - if self.token_type == JWT: - if self.token is not None: - assert self.token_expires_at is not None - if time.time() < self.token_expires_at - 60: - # Assumption: the token is valid; unless the token has - # been revoked, this should be correct. - return - # The token has expired or is about to expire in less than - # one minute. Clear it. We assume that the token in the - # persistent cache or in TRAVO_TOKEN, if present, are the - # same. This is correct unless there is a concurrent travo - # process. So clear then as well. - self.logout() - - # Try to retrieve token from environment variable - if "TRAVO_TOKEN" in os.environ: - if self.set_token(os.environ["TRAVO_TOKEN"], nosave=True): - return - raise AuthenticationError( - _("invalid token environment variable", url=self.base_url) - ) + if self.token is not None: + assert self.token_expires_at is not None + if time.time() < self.token_expires_at - 60: + # Assumption: the token is valid; unless the token has + # been revoked, this should be correct. + return + # The token has expired or is about to expire in less than + # one minute. Clear it. We assume that the token in the + # persistent cache or in TRAVO_TOKEN, if present, are the + # same. This is correct unless there is a concurrent travo + # process. So clear then as well. + self.logout() - # Try to retrieve token from persistent cache - token_file = self.token_file() - if os.path.exists(token_file): - if self.set_token(io.open(token_file).read().rstrip()): - return - self.logout() + # Try to retrieve token from environment variable + if "TRAVO_TOKEN" in os.environ: + if self.set_token(os.environ["TRAVO_TOKEN"], nosave=True): + return + raise AuthenticationError( + _("invalid token environment variable", url=self.base_url) + ) - # No token available + # Try to retrieve token from persistent cache + token_file = self.token_file() + if os.path.exists(token_file): + if self.set_token(io.open(token_file).read().rstrip()): + return + self.logout() + + # No token available + if self.token_type == JWT: if username is None: if self._current_user is anonymous_user and anonymous_ok: return @@ -319,35 +317,6 @@ class GitLab: assert self.set_token(token) elif self.token_type == PERSONAL_ACCESS: - # if self.token is not None: - # assert self.token_expires_at is not None - # if time.time() < self.token_expires_at - 60: - # Assumption: the token is valid; unless the token has - # been revoked, this should be correct. - # return - # The token has expired or is about to expire in less than - # one minute. Clear it. We assume that the token in the - # persistent cache or in TRAVO_TOKEN, if present, are the - # same. This is correct unless there is a concurrent travo - # process. So clear then as well. - # self.logout() - - # Try to retrieve token from environment variable - if "GITLAB_PRIVATE_TOKEN" in os.environ: - if self.set_token(os.environ["GITLAB_PRIVATE_TOKEN"], nosave=False): - return - raise AuthenticationError( - _("invalid token environment variable", url=self.base_url) - ) - - # Try to retrieve token from persistent cache - token_file = self.token_file() - if os.path.exists(token_file): - if self.set_token(io.open(token_file).read().rstrip()): - return - self.logout() - - # No token available token = getpass.getpass(_("personal access token") + ": ") if token is None: # TODO: pourrait réessayer @@ -900,10 +869,7 @@ class GitLab: if not anonymous: self.login(anonymous_ok=anonymous_ok) if self.get_current_user() is not anonymous_user: - if self.token_type == JWT: - env["TRAVO_TOKEN"] = cast(str, self.token) - elif self.token_type == PERSONAL_ACCESS: - env["GITLAB_PRIVATE_TOKEN"] = cast(str, self.token) + env["TRAVO_TOKEN"] = cast(str, self.token) env["GIT_ASKPASS"] = "travo-echo-travo-token" git = ["git", "-c", "credential.username=oauth2"] return run(git + list(args), env=env, **kargs) diff --git a/travo/tests/test_console_scripts.py b/travo/tests/test_console_scripts.py index 6e0ffb5..e8cae96 100644 --- a/travo/tests/test_console_scripts.py +++ b/travo/tests/test_console_scripts.py @@ -16,12 +16,11 @@ def test_quickstart(embed_option, tmp_path): assert os.path.isdir(clab_dir + "/ComputerLab") -@pytest.mark.parametrize("token", ["GITLAB_PRIVATE_TOKEN", "TRAVO_TOKEN"]) -def test_echo_token(capsys, token): +def test_echo_token(capsys): personal_token = "personal_token" - os.environ[token] = personal_token + os.environ["TRAVO_TOKEN"] = personal_token travo_echo_travo_token() captured = capsys.readouterr().out.rstrip() assert captured == personal_token - del os.environ[token] + del os.environ["TRAVO_TOKEN"] -- GitLab From 7b01bebf2cb2ce4b002ef4575cdef407aa15a7b5 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Mon, 10 Nov 2025 14:42:57 +0100 Subject: [PATCH 14/26] Add some tests --- travo/gitlab.py | 2 +- travo/tests/test_gitlab.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/travo/gitlab.py b/travo/gitlab.py index 1d30509..6d10241 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -170,7 +170,7 @@ class GitLab: del self.session.headers["Authorization"] if "TRAVO_TOKEN" in os.environ: del os.environ["TRAVO_TOKEN"] - if "PRIVATE_TOKEN" in self.session.headers: + if "PRIVATE-TOKEN" in self.session.headers: del self.session.headers["PRIVATE-TOKEN"] token_file = self.token_file() # Testing whether the file exists before removing it is not diff --git a/travo/tests/test_gitlab.py b/travo/tests/test_gitlab.py index bfc4a98..da28d2e 100644 --- a/travo/tests/test_gitlab.py +++ b/travo/tests/test_gitlab.py @@ -24,6 +24,17 @@ def test_request_credentials(monkeypatch: pytest.MonkeyPatch) -> None: assert password == "" +def test_set_token() -> None: + forge = gitlab.GitLab( + base_url="https://gitlab.example.com", token_type="PERSONAL_ACCESS" + ) + personal_token = "personal_token" + forge.set_token(personal_token) + assert forge.session.headers["PRIVATE-TOKEN"] == personal_token + forge.logout() + assert "PRIVATE-TOKEN" not in forge.session.headers.keys() + + def test_token_requests(gitlab_url: str) -> None: forge = gitlab.GitLab(base_url=gitlab_url) assert not forge.set_token("*Ae", nosave=True) -- GitLab From 57081cf5692dcc0a15d07db55d96d976d3715431 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Mon, 10 Nov 2025 15:36:34 +0100 Subject: [PATCH 15/26] Test authentication errors --- travo/gitlab.py | 7 +++++++ travo/tests/test_gitlab.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/travo/gitlab.py b/travo/gitlab.py index 6d10241..84ab1dc 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -227,6 +227,13 @@ class GitLab: Login. Here the credentials are passed as parameters; they typically are instead entered interactively by the user: + >>> gitlab.login(username="student1", + ... password="aqwzsx(") + + Traceback (most recent call last): + ... + travo.gitlab.AuthenticationError: Authentification failed; invalid username or password? + >>> gitlab.login(username="student1", ... password="aqwzsx(t1") diff --git a/travo/tests/test_gitlab.py b/travo/tests/test_gitlab.py index da28d2e..d4bfc45 100644 --- a/travo/tests/test_gitlab.py +++ b/travo/tests/test_gitlab.py @@ -28,6 +28,12 @@ def test_set_token() -> None: forge = gitlab.GitLab( base_url="https://gitlab.example.com", token_type="PERSONAL_ACCESS" ) + personal_token = None + with pytest.raises( + gitlab.AuthenticationError, + match="Authentification failed; invalid username or password?", + ): + forge.set_token(personal_token) personal_token = "personal_token" forge.set_token(personal_token) assert forge.session.headers["PRIVATE-TOKEN"] == personal_token -- GitLab From 864f750e982cbcb4904913fda5393aef08718f5c Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Mon, 10 Nov 2025 19:03:16 +0100 Subject: [PATCH 16/26] Cleaning and improve coverage --- travo/gitlab.py | 6 +----- travo/locale/travo.en.yml | 4 ++-- travo/tests/test_gitlab.py | 6 ------ 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/travo/gitlab.py b/travo/gitlab.py index 84ab1dc..b04e186 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -229,10 +229,9 @@ class GitLab: >>> gitlab.login(username="student1", ... password="aqwzsx(") - Traceback (most recent call last): ... - travo.gitlab.AuthenticationError: Authentification failed; invalid username or password? + AuthenticationError: Authentication failed; invalid username or password? >>> gitlab.login(username="student1", ... password="aqwzsx(t1") @@ -325,9 +324,6 @@ class GitLab: elif self.token_type == PERSONAL_ACCESS: token = getpass.getpass(_("personal access token") + ": ") - if token is None: - # TODO: pourrait réessayer - raise AuthenticationError(_("invalid credentials", url=self.base_url)) assert self.set_token(token) def get(self, path: str, data: dict = {}) -> requests.Response: diff --git a/travo/locale/travo.en.yml b/travo/locale/travo.en.yml index 5f14b57..6f7bd42 100644 --- a/travo/locale/travo.en.yml +++ b/travo/locale/travo.en.yml @@ -47,10 +47,10 @@ en: show command log: "Show the log of the commands" command log level: "Configure the log level of the commands" student group: "Student group" - invalid credentials: "Authentification failed; invalid username or password?" + invalid credentials: "Authentication failed; invalid username or password?" invalid token environment variable: "Invalid or expired TRAVO_TOKEN environment variable." sign in: "Sign in" - authentication required: "Authentification required" + authentication required: "Authentication required" abort conflicting merge: "Integrating %{content} would cause a conflit; abandon" updates: "updates" erratas: erratas diff --git a/travo/tests/test_gitlab.py b/travo/tests/test_gitlab.py index d4bfc45..da28d2e 100644 --- a/travo/tests/test_gitlab.py +++ b/travo/tests/test_gitlab.py @@ -28,12 +28,6 @@ def test_set_token() -> None: forge = gitlab.GitLab( base_url="https://gitlab.example.com", token_type="PERSONAL_ACCESS" ) - personal_token = None - with pytest.raises( - gitlab.AuthenticationError, - match="Authentification failed; invalid username or password?", - ): - forge.set_token(personal_token) personal_token = "personal_token" forge.set_token(personal_token) assert forge.session.headers["PRIVATE-TOKEN"] == personal_token -- GitLab From 14c4b98390146085f385469c59baa9ac7ad01bc6 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Mon, 10 Nov 2025 20:45:22 +0100 Subject: [PATCH 17/26] Add test login with personal token. --- travo/tests/test_gitlab.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/travo/tests/test_gitlab.py b/travo/tests/test_gitlab.py index da28d2e..54820eb 100644 --- a/travo/tests/test_gitlab.py +++ b/travo/tests/test_gitlab.py @@ -24,6 +24,18 @@ def test_request_credentials(monkeypatch: pytest.MonkeyPatch) -> None: assert password == "" +def test_login_with_personal_access(monkeypatch: pytest.MonkeyPatch) -> None: + forge = gitlab.GitLab( + base_url="https://gitlab.example.com", token_type="PERSONAL_ACCESS" + ) + forge.logout() + inputs = iter(["personal_token"]) + monkeypatch.setattr(getpass, "getpass", lambda _: next(inputs)) + forge.login() + assert forge.token == "personal_token" + forge.logout() + + def test_set_token() -> None: forge = gitlab.GitLab( base_url="https://gitlab.example.com", token_type="PERSONAL_ACCESS" -- GitLab From 0231625145254c6c692ac7763257cca27606af16 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Mon, 10 Nov 2025 23:21:24 +0100 Subject: [PATCH 18/26] Fix token expiration, only relevant for JWT --- travo/gitlab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/travo/gitlab.py b/travo/gitlab.py index b04e186..761dccc 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -266,7 +266,7 @@ class GitLab: {'emoji': None, 'message': None, 'availability': None, 'message_html': ''... """ - if self.token is not None: + if self.token is not None and self.token_type == JWT: assert self.token_expires_at is not None if time.time() < self.token_expires_at - 60: # Assumption: the token is valid; unless the token has -- GitLab From bf24ab0a0941b353a6cdbd65551f370553aee633 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 12 Nov 2025 10:39:19 +0100 Subject: [PATCH 19/26] Add coverage for user none login --- travo/tests/test_gitlab.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/travo/tests/test_gitlab.py b/travo/tests/test_gitlab.py index 54820eb..9ba8206 100644 --- a/travo/tests/test_gitlab.py +++ b/travo/tests/test_gitlab.py @@ -56,6 +56,20 @@ def test_token_requests(gitlab_url: str) -> None: forge.set_token("very_secret_token", nosave=True) +def test_login_username_none(gitlab_url: str, monkeypatch: pytest.MonkeyPatch) -> None: + forge = gitlab.GitLab(base_url=gitlab_url) + forge.login(anonymous_ok=True) + token = forge.token + assert token is None + forge.logout() + + inputs = iter(["travo-test-etu", "aqwzsx(t1"]) + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + forge.login() + assert token is not None + forge.logout() + + def test_login_logout(gitlab_url: str) -> None: forge = gitlab.GitLab(base_url=gitlab_url) forge.login(username="anonymous", anonymous_ok=True) -- GitLab From 453efa2891c1d796000204a32edb8c37f3a3854e Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 12 Nov 2025 10:59:07 +0100 Subject: [PATCH 20/26] Fix monkeypatching --- travo/tests/test_gitlab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/travo/tests/test_gitlab.py b/travo/tests/test_gitlab.py index 9ba8206..9419302 100644 --- a/travo/tests/test_gitlab.py +++ b/travo/tests/test_gitlab.py @@ -64,7 +64,7 @@ def test_login_username_none(gitlab_url: str, monkeypatch: pytest.MonkeyPatch) - forge.logout() inputs = iter(["travo-test-etu", "aqwzsx(t1"]) - monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + monkeypatch.setattr("sys.stdin", "input", lambda _: next(inputs)) forge.login() assert token is not None forge.logout() -- GitLab From bf4c0845117ac27fdb837c62313b60fa97e9f0bb Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 12 Nov 2025 11:17:57 +0100 Subject: [PATCH 21/26] Try again --- travo/tests/test_gitlab.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/travo/tests/test_gitlab.py b/travo/tests/test_gitlab.py index 9419302..cccf2ab 100644 --- a/travo/tests/test_gitlab.py +++ b/travo/tests/test_gitlab.py @@ -8,11 +8,11 @@ from travo import gitlab def test_request_credentials(monkeypatch: pytest.MonkeyPatch) -> None: forge = gitlab.GitLab(base_url="https://gitlab.example.com") inputs = iter(["travo-test-etu", "aqwzsx(t1"]) - with pytest.raises(OSError, match="reading from stdin while output is captured"): - user, passwd = gitlab.request_credentials_basic(forge) - monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) - assert user == "travo-test-etu" - assert passwd == "aqwzsx(t1" + # with pytest.raises(OSError, match="reading from stdin while output is captured"): + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + user, passwd = gitlab.request_credentials_basic(forge) + assert user == "travo-test-etu" + assert passwd == "aqwzsx(t1" monkeypatch.setattr(getpass, "getpass", lambda _: "aqwzsx(t1") user, passwd = gitlab.request_credentials_basic(forge, username="travo-test-etu") @@ -64,10 +64,11 @@ def test_login_username_none(gitlab_url: str, monkeypatch: pytest.MonkeyPatch) - forge.logout() inputs = iter(["travo-test-etu", "aqwzsx(t1"]) - monkeypatch.setattr("sys.stdin", "input", lambda _: next(inputs)) - forge.login() - assert token is not None - forge.logout() + with pytest.raises(OSError, match="reading from stdin while output is captured"): + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + forge.login() + assert token is not None + forge.logout() def test_login_logout(gitlab_url: str) -> None: -- GitLab From 0ec01cea630564edbe5313369741ab300478b9ff Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 12 Nov 2025 14:16:28 +0100 Subject: [PATCH 22/26] Try again --- travo/tests/test_gitlab.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/travo/tests/test_gitlab.py b/travo/tests/test_gitlab.py index cccf2ab..f82a5c3 100644 --- a/travo/tests/test_gitlab.py +++ b/travo/tests/test_gitlab.py @@ -8,11 +8,11 @@ from travo import gitlab def test_request_credentials(monkeypatch: pytest.MonkeyPatch) -> None: forge = gitlab.GitLab(base_url="https://gitlab.example.com") inputs = iter(["travo-test-etu", "aqwzsx(t1"]) - # with pytest.raises(OSError, match="reading from stdin while output is captured"): - monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) - user, passwd = gitlab.request_credentials_basic(forge) - assert user == "travo-test-etu" - assert passwd == "aqwzsx(t1" + with pytest.raises(OSError, match="reading from stdin while output is captured"): + monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) + user, passwd = gitlab.request_credentials_basic(forge) + assert user == "travo-test-etu" + assert passwd == "aqwzsx(t1" monkeypatch.setattr(getpass, "getpass", lambda _: "aqwzsx(t1") user, passwd = gitlab.request_credentials_basic(forge, username="travo-test-etu") -- GitLab From 0825abf2a04445801deedf4364f3736a5d28f154 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 12 Nov 2025 15:22:10 +0100 Subject: [PATCH 23/26] Rename and reorder anonymous login --- travo/tests/test_gitlab.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/travo/tests/test_gitlab.py b/travo/tests/test_gitlab.py index f82a5c3..531c3b8 100644 --- a/travo/tests/test_gitlab.py +++ b/travo/tests/test_gitlab.py @@ -58,24 +58,25 @@ def test_token_requests(gitlab_url: str) -> None: def test_login_username_none(gitlab_url: str, monkeypatch: pytest.MonkeyPatch) -> None: forge = gitlab.GitLab(base_url=gitlab_url) - forge.login(anonymous_ok=True) - token = forge.token - assert token is None forge.logout() inputs = iter(["travo-test-etu", "aqwzsx(t1"]) with pytest.raises(OSError, match="reading from stdin while output is captured"): monkeypatch.setattr(builtins, "input", lambda _: next(inputs)) forge.login() + token = forge.token assert token is not None forge.logout() -def test_login_logout(gitlab_url: str) -> None: +def test_login_anonymous(gitlab_url: str) -> None: forge = gitlab.GitLab(base_url=gitlab_url) forge.login(username="anonymous", anonymous_ok=True) token = forge.token assert token is None + forge.login(anonymous_ok=True) + token = forge.token + assert token is None def test_get_user_nousername(gitlab): -- GitLab From 8598e7ca508fe3e695204f07c0edea2996419cb3 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 12 Nov 2025 15:48:32 +0100 Subject: [PATCH 24/26] Add link and instructions to prompt --- travo/gitlab.py | 7 +++++++ travo/locale/travo.en.yml | 1 + travo/locale/travo.fr.yml | 1 + 3 files changed, 9 insertions(+) diff --git a/travo/gitlab.py b/travo/gitlab.py index 761dccc..e7dfd70 100644 --- a/travo/gitlab.py +++ b/travo/gitlab.py @@ -323,6 +323,13 @@ class GitLab: assert self.set_token(token) elif self.token_type == PERSONAL_ACCESS: + print( + _("personal access token") + + ".\n" + + _("no personal access token") + + "\n" + + f"`{self.base_url}/-/user_settings/personal_access_tokens`" + ) token = getpass.getpass(_("personal access token") + ": ") assert self.set_token(token) diff --git a/travo/locale/travo.en.yml b/travo/locale/travo.en.yml index 6f7bd42..779777b 100644 --- a/travo/locale/travo.en.yml +++ b/travo/locale/travo.en.yml @@ -129,3 +129,4 @@ en: no content: "No %{content}" corrupted assignment directory: "Corrupted assignment directory" personal access token: "Enter your personal access token" + no personal access token: "If you don't have one, please create it from" diff --git a/travo/locale/travo.fr.yml b/travo/locale/travo.fr.yml index 19e4da7..073535d 100644 --- a/travo/locale/travo.fr.yml +++ b/travo/locale/travo.fr.yml @@ -129,3 +129,4 @@ fr: your submission: "Votre submission" corrupted assignment directory: "Corrupted assignment directory" personal access token: "Tapez votre jeton d'accès personnel" + no personal access token: "Si vous n'en avez pas, veuillez en créer un à partir de" -- GitLab From 2571c1655e89b09ce5a9d3a7d0c410cd04a5e0da Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Wed, 12 Nov 2025 16:11:34 +0100 Subject: [PATCH 25/26] Add changelog --- docs/sources/changelog.md | 8 ++++++++ travo/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/sources/changelog.md b/docs/sources/changelog.md index ed8e77e..42390c9 100644 --- a/docs/sources/changelog.md +++ b/docs/sources/changelog.md @@ -1,5 +1,13 @@ # What's new? +## Version 2.0.0 + +### Features + +- Authentification via personal access token is now possible. + This is mandatory when double authentication or Single-Sign-On + is enabled on the forge + ## Version 1.1.3 Fix missing `pytest` dependency. diff --git a/travo/__init__.py b/travo/__init__.py index 4419e45..e0be669 100644 --- a/travo/__init__.py +++ b/travo/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.2.0" +__version__ = "2.0.0" try: from ._version import __version__ -- GitLab From 03b89b59d36a4ff1addd0e021c09c9d703ff1c04 Mon Sep 17 00:00:00 2001 From: Chiara Marmo Date: Mon, 17 Nov 2025 15:22:36 +0100 Subject: [PATCH 26/26] Add documentation about authentication --- docs/sources/authentication.md | 28 ++++++++++++++++++++++++++++ docs/sources/index.md | 1 + 2 files changed, 29 insertions(+) create mode 100644 docs/sources/authentication.md diff --git a/docs/sources/authentication.md b/docs/sources/authentication.md new file mode 100644 index 0000000..fe948ce --- /dev/null +++ b/docs/sources/authentication.md @@ -0,0 +1,28 @@ +# Access your forge + +`Travo` goal is to easily interact with a forge, for that, before starting using +`Travo`, it is essential to verify that the forge is accessible and the user can +successfully authenticate into it. + +`Travo` communicates with the forge via securized http transactions. +It supports two types of authentication: +- [basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) via user and password credentials; +- [personal access token](https://en.wikipedia.org/wiki/Personal_access_token) +authentication. + +### Password authentication +`Travo` asks for user and password in order to authenticate. + +Please note that the user should have authenticate into the forge at least once for `Travo` +to log in successfully. + +### Personal access token authentication +Personal access token (PAT) authentication is required when two-factor authentication (2FA) +or SAML Single Sign-On (SSO) are enabled on the forge: users authenticate with a personal +access token in place of the password. +Username is not evaluated as part of the authentication process. + +The user needs to create a Personal Access Token, first, as described, for example, +in the [related gitlab documentation](https://docs.gitlab.com/user/profile/personal_access_tokens/#create-a-personal-access-token). +For `Travo` to be able to interact with the forge the [scope](https://docs.gitlab.com/user/profile/personal_access_tokens/#personal-access-token-scopes) +of the token should be set to `api`. \ No newline at end of file diff --git a/docs/sources/index.md b/docs/sources/index.md index 4121795..0b93d85 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -155,6 +155,7 @@ hidden: --- changelog install +authentication quickstart_tutorial.rst Assignment Tutorial Advanced course tutorial -- GitLab