diff --git a/cadetrdm/remote_integration.py b/cadetrdm/remote_integration.py index 6857d76364d992f713435e2b5fae5904e3ec8fac..3947f5183a2dd9cb64d4df2d93ca91d36d6f5cbc 100644 --- a/cadetrdm/remote_integration.py +++ b/cadetrdm/remote_integration.py @@ -1,76 +1,137 @@ import gitlab import github +import keyring +from abc import abstractmethod -def load_token(): - """ - Read the API token from the .token file - - :return: - """ - with open("../.token", "r") as file_handle: - token = file_handle.readline() - return token - - -def create_gitlab_remote(namespace, name, url=None): - token = load_token() - - gl = gitlab.Gitlab(url, private_token=token) - - namespace_id = gl.namespaces.list(get_all=True, search=namespace)[0].id - response = gl.projects.create({"name": name, "namespace_id": namespace_id}) - return response - - -def delete_gitlab_remote(url, namespace, name): - token = load_token() - - gl = gitlab.Gitlab(url, private_token=token) - - potential_projects = gl.projects.list(get_all=True, search=[namespace, name]) - - for project in potential_projects: - if project.name != name: - pass - if project.namespace["name"] != namespace: - pass - - gl.projects.delete(project.id) - - -def create_github_remote(name, namespace=None, url="https://api.github.com"): - token = load_token() - - auth = github.Auth.Token(token) - g = github.Github(base_url=url, auth=auth) - user = g.get_user() - - if namespace is None or namespace == user.login: - base = user - else: +class Remote: + @staticmethod + def load_token(url_options, username): + token = None + url_options = iter(url_options) try: - organization = g.get_organization(namespace) - base = organization - except github.GithubException: - raise RuntimeError(f"No organization or user named {namespace} found in {url}") - - response = base.create_repo( - name, - allow_rebase_merge=True, - auto_init=False, - has_issues=True, - has_projects=False, - has_wiki=False, - private=False, - ) - return response - - -def delete_github_remote(name, namespace, url="https://api.github.com"): - token = load_token() - - auth = github.Auth.Token(token) - g = github.Github(base_url=url, auth=auth) - repo = g.get_repo(f"{namespace}/{name}") - repo.delete() + while token is None: + token = keyring.get_password(next(url_options), username) + except StopIteration: + raise RuntimeError(f"No token found in keyring for url {url_options[0]} and username {username}") + + return token + + @abstractmethod + def create_remote(self, url, namespace, name, username): + return + + @abstractmethod + def delete_remote(self, url, namespace, name, username): + return + + +class GitLabRemote(Remote): + + @property + def url_fallbacks(self): + return ["gitlab"] + + def create_remote(self, url, namespace, name, username): + """ + Create remotes on gitlab within the given url / namespace / name. Use the token + stored in the keyring under the username and url combination. + + :param namespace: + :param name: + :param url: + :param username: + :return: + Query response + """ + token = self.load_token([url] + self.url_fallbacks, username) + gl = gitlab.Gitlab(url, private_token=token) + + namespace_id = gl.namespaces.list(get_all=True, search=namespace)[0].id + response = gl.projects.create({"name": name, "namespace_id": namespace_id}) + return response + + def delete_remote(self, url, namespace, name, username): + """ + Deletes remotes on gitlab within the given url / namespace / name. Use the token + stored in the keyring under the username and url combination. + + :param namespace: + :param name: + :param url: + :param username: + :return: + None + """ + token = self.load_token([url] + self.url_fallbacks, username) + gl = gitlab.Gitlab(url, private_token=token) + + potential_projects = gl.projects.list(get_all=True, search=[namespace, name]) + + for project in potential_projects: + if project.name != name: + pass + if project.namespace["name"] != namespace: + pass + + gl.projects.delete(project.id) + return + + +class GitHubRemote(Remote): + + @property + def url_fallbacks(self): + return ["https://github.com/", "https://github.com", "github", "github.com"] + + def create_remote(self, name, namespace=None, url="https://api.github.com", username=None): + """ + Create remotes on GitHub within the given url / namespace / name. Use the token + stored in the keyring under the username and url combination. + + :param namespace: + :param name: + :param url: + :param username: + :return: + Query response + """ + if username is None and namespace is not None: + username = namespace + + token = self.load_token([url] + self.url_fallbacks, username) + + auth = github.Auth.Token(token) + g = github.Github(base_url=url, auth=auth) + user = g.get_user() + + if namespace is None or namespace == user.login: + base = user + else: + try: + organization = g.get_organization(namespace) + base = organization + except github.GithubException: + raise RuntimeError(f"No organization or user named {namespace} found in {url}") + + response = base.create_repo( + name, + allow_rebase_merge=True, + auto_init=False, + has_issues=True, + has_projects=False, + has_wiki=False, + private=False, + ) + return response + + def delete_remote(self, name, namespace, url="https://api.github.com", username=None): + if username is None: + username = namespace + + token = self.load_token([url] + self.url_fallbacks, username) + + auth = github.Auth.Token(token) + g = github.Github(base_url=url, auth=auth) + repo = g.get_repo(f"{namespace}/{name}") + repo.delete() diff --git a/cadetrdm/repositories.py b/cadetrdm/repositories.py index b204ad0fd608595c36f657cd83d1a1dc1f1bef2e..1c5fed3857b9ef8decdb74d8e2e2c6d2278b92b7 100644 --- a/cadetrdm/repositories.py +++ b/cadetrdm/repositories.py @@ -15,7 +15,7 @@ from tabulate import tabulate from cadetrdm.io_utils import recursive_chmod, write_lines_to_file, wait_for_user, init_lfs from cadetrdm.jupyter_functionality import Notebook -from cadetrdm.remote_integration import create_gitlab_remote, create_github_remote +from cadetrdm.remote_integration import GitHubRemote, GitLabRemote from cadetrdm.version import version as cadetrdm_version try: @@ -622,36 +622,27 @@ class ProjectRepo(BaseRepo): jupytext_lines = ['# Pair ipynb notebooks to py:percent text notebooks', 'formats: "ipynb,py:percent"'] write_lines_to_file(Path(path_root) / "jupytext.yml", lines=jupytext_lines, open_type="w") - def create_gitlab_remotes(self, name, namespace, url=None): + def create_remotes(self, name, namespace, url=None, username=None): """ Create project in gitlab and add the projects as remotes to the project and output repositories + :param username: :param url: :param namespace: :param name: :return: """ - response_project = create_gitlab_remote(url=url, namespace=namespace, name=name) - response_output = create_gitlab_remote(url=url, namespace=namespace, name=name + "_output") + if "github" in url: + remote = GitHubRemote() + else: + remote = GitLabRemote() + + response_project = remote.create_remote(url=url, namespace=namespace, name=name, username=username) + response_output = remote.create_remote(url=url, namespace=namespace, name=name + "_output", username=username) self.add_remote(response_project.ssh_url_to_repo) self.output_repo.add_remote(response_output.ssh_url_to_repo) self.push(push_all=True) - def create_github_remotes(self, name, namespace=None, url="https://api.github.com"): - """ - Create project in GitHub and add the projects as remotes to the project and output repositories - - :param namespace: - :param name: - :param url: - :return: - """ - response_project = create_github_remote(namespace=namespace, name=name, url=url) - response_output = create_github_remote(namespace=namespace, name=name + "_output", url=url) - self.add_remote(response_project.html_url) - self.output_repo.add_remote(response_output.html_url) - self.push(push_all=True) - def get_new_output_branch_name(self): """ Construct a name for the new branch in the output repository. diff --git a/docs/source/user_guide/getting-started.md b/docs/source/user_guide/getting-started.md index fff79444b70c07f8bedbbc01255b0c8d95929734..19c433c44cd4b110371913ee9cb85018c18b4f17 100644 --- a/docs/source/user_guide/getting-started.md +++ b/docs/source/user_guide/getting-started.md @@ -23,12 +23,20 @@ The `output_folder_name` can be given optionally. It defaults to `output`. You can create remotes for both the project and the output repository with one command, using the GitLab or GitHub API. -Using the GitLab API requires you to have created a -[GitLab Personal Access Token (PAT)](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html) -and to store it in the `.token` file in the project's root. Then you can run: +You need to create a +[GitLab Personal Access Token (PAT)](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html) or [GitHub PAT](https://github.com/settings/tokens?type=beta) +and to store it in the Python `keyring` using ```python -repo.create_gitlab_remotes( +import keyring + +keyring.set_password("e.g. https://jugit.fz-juelich.de/", username, token) +``` + +Then you can run: + +```python +repo.create_remotes( name="e.g. API_test_project", namespace="e.g. r.jaepel", url="e.g. https://jugit.fz-juelich.de/", @@ -38,12 +46,10 @@ repo.create_gitlab_remotes( or ```bash -cadet-rdm create-gitlab-remotes API_test_project r.jaepel https://jugit.fz-juelich.de/ +cadet-rdm create-remotes name namespace url username +cadet-rdm create-remotes API_test_project r.jaepel https://jugit.fz-juelich.de/ r.jaepel ``` -Both functions are also available for the GitHub API, which will require a -[GitHub PAT](https://github.com/settings/tokens?type=beta): -`repo.create_github_remotes(name, namespace)` and ` cadet-rdm create-github-remotes name namespace`. ## Extending GIT-LFS scope diff --git a/setup.cfg b/setup.cfg index 2560ffbf1ea1be1fdf06faa34321ef59a9d6bdc8..e3b27830093d9fba38a4f00b920024200d2403ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,7 @@ install_requires = git-lfs click tabulate + keyring include_package_data = True diff --git a/tests/test_gitlab_api.py b/tests/test_gitlab_api.py index 792b023dfb7e4a0a164ffdf97910da21e5c71cdc..15e467ee178a51d31fac587625e803533fc14996 100644 --- a/tests/test_gitlab_api.py +++ b/tests/test_gitlab_api.py @@ -6,16 +6,17 @@ import pytest from cadetrdm import initialize_repo, ProjectRepo from cadetrdm.io_utils import delete_path -from cadetrdm.remote_integration import delete_gitlab_remote, create_gitlab_remote +from cadetrdm.remote_integration import GitHubRemote, GitLabRemote def test_gitlab_create(): url = "https://jugit.fz-juelich.de/" namespace = "r.jaepel" name = "API_test_project" + remote = GitLabRemote() # ensure remote does not exist - delete_gitlab_remote(url=url, namespace=namespace, name=name) + remote.delete_remote(url=url, namespace=namespace, name=name, username="r.jaepel") try: delete_path("test_repo_remote") except FileNotFoundError: @@ -23,46 +24,46 @@ def test_gitlab_create(): sleep(3) - response = create_gitlab_remote(url=url, namespace=namespace, name=name) + response = remote.create_remote(url=url, namespace=namespace, name=name, username="r.jaepel") git.Repo.clone_from(response.ssh_url_to_repo, "test_repo_remote") delete_path("test_repo_remote") - delete_gitlab_remote(url=url, namespace=namespace, name=name) + remote.delete_remote(url=url, namespace=namespace, name=name, username="r.jaepel") with pytest.raises(git.exc.GitCommandError): git.Repo.clone_from(response.ssh_url_to_repo, "test_repo_remote") -# def test_github_create(): -# from cadetrdm.remote_integration import delete_github_remote, create_github_remote -# namespace = "ronald-jaepel" -# name = "API_test_project" -# -# # ensure remote does not exist -# try: -# delete_github_remote(namespace=namespace, name=name) -# except Exception: -# pass -# -# try: -# delete_path("test_repo_remote") -# except FileNotFoundError: -# pass -# -# sleep(3) -# -# response = create_github_remote(namespace=namespace, name=name) -# -# sleep(3) -# -# git.Repo.clone_from(response.html_url, "test_repo_remote") -# delete_path("test_repo_remote") -# -# delete_github_remote(namespace=namespace, name=name) -# -# with pytest.raises(git.exc.GitCommandError): -# git.Repo.clone_from(response.ssh_url_to_repo, "test_repo_remote") +def test_github_create(): + namespace = "ronald-jaepel" + name = "API_test_project" + remote = GitHubRemote() + + # ensure remote does not exist + try: + remote.delete_remote(namespace=namespace, name=name, username="r.jaepel") + except Exception: + pass + + try: + delete_path("test_repo_remote") + except FileNotFoundError: + pass + + sleep(3) + + response = remote.create_remote(namespace=namespace, name=name, username="r.jaepel") + + sleep(3) + + git.Repo.clone_from(response.html_url, "test_repo_remote") + delete_path("test_repo_remote") + + remote.delete_remote(namespace=namespace, name=name, username="r.jaepel") + + with pytest.raises(git.exc.GitCommandError): + git.Repo.clone_from(response.ssh_url, "test_repo_remote") def test_repo_gitlab_integration(): @@ -70,10 +71,11 @@ def test_repo_gitlab_integration(): namespace = "r.jaepel" name = "API_test_project" repo_name = "test_repo_remote" + remote = GitLabRemote() # Clean up - delete_gitlab_remote(url=url, namespace=namespace, name=name) - delete_gitlab_remote(url=url, namespace=namespace, name=name + "_output") + remote.delete_remote(url=url, namespace=namespace, name=name, username="r.jaepel") + remote.delete_remote(url=url, namespace=namespace, name=name + "_output", username="r.jaepel") try: delete_path("test_repo_remote") @@ -83,4 +85,4 @@ def test_repo_gitlab_integration(): initialize_repo(repo_name) repo = ProjectRepo(repo_name) - repo.create_gitlab_remotes(url=url, namespace=namespace, name=name) + repo.create_remotes(url=url, namespace=namespace, name=name, username="r.jaepel")