├── .gitignore ├── .gitlab-ci.yml ├── Dockerfile ├── LICENSE ├── README.md ├── example.svg ├── mirrormaker ├── __init__.py ├── github.py ├── gitlab.py └── mirrormaker.py ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py └── test_mirrormaker.py /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | __pycache__/ 3 | .pytest_cache/ 4 | *.egg-info/ -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | - publish 4 | 5 | .python_defaults: 6 | image: python:3.8-slim 7 | 8 | variables: 9 | PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" 10 | 11 | cache: 12 | paths: 13 | - .cache/pip 14 | - .venv/ 15 | 16 | before_script: 17 | - python -V 18 | - pip install poetry 19 | - poetry --version 20 | - poetry config virtualenvs.in-project true 21 | - poetry install -vv 22 | 23 | test: 24 | stage: test 25 | extends: .python_defaults 26 | script: 27 | - poetry run pytest 28 | except: 29 | - schedules 30 | 31 | publish_pypi: 32 | stage: publish 33 | extends: .python_defaults 34 | script: 35 | - poetry publish --build --username $PYPI_USER --password $PYPI_PASSWORD 36 | only: 37 | - master 38 | except: 39 | - schedules 40 | 41 | publish_docker: 42 | stage: publish 43 | image: docker:19.03.1 44 | services: 45 | - docker:19.03.1-dind 46 | 47 | before_script: 48 | - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY 49 | 50 | script: 51 | - VERSION=`grep "version.*=.*" pyproject.toml | cut -d\" -f2` 52 | - docker build -t $CI_REGISTRY_IMAGE:$VERSION -t $CI_REGISTRY_IMAGE:latest . 53 | - docker push $CI_REGISTRY_IMAGE:$VERSION 54 | - docker push $CI_REGISTRY_IMAGE:latest 55 | 56 | only: 57 | - master 58 | except: 59 | - schedules 60 | 61 | scheduled_gitlab_mirror_maker: 62 | image: python:3.8-alpine 63 | script: 64 | - pip install gitlab-mirror-maker 65 | - gitlab-mirror-maker 66 | only: 67 | - schedules 68 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim as builder 2 | COPY . . 3 | RUN pip install poetry && rm -rf dist && poetry build -f wheel 4 | 5 | 6 | FROM python:3.8-alpine 7 | COPY --from=builder /dist /dist 8 | RUN pip install /dist/* 9 | ENTRYPOINT ["gitlab-mirror-maker"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Grzegorz Dlugoszewski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitLab Mirror Maker 2 | 3 | GitLab Mirror Maker is a small tool written in Python that automatically mirrors your public repositories from GitLab to GitHub. 4 | 5 | ![Example](./example.svg) 6 | 7 | 8 | # Why? 9 | 10 | - Maybe you like GitLab better but the current market favors developers with a strong GitHub presence? 11 | - Maybe as a form of backup? 12 | - Or maybe you have other reasons... :wink: 13 | 14 | 15 | # Installation 16 | 17 | Install with pip or pipx: 18 | ``` 19 | pip install gitlab-mirror-maker 20 | ``` 21 | 22 | There's also a Docker image available: 23 | ``` 24 | docker run registry.gitlab.com/grdl/gitlab-mirror-maker 25 | ``` 26 | 27 | 28 | # Usage 29 | 30 | Run: `gitlab-mirror-maker --github-token xxx --gitlab-token xxx` 31 | 32 | See [Authentication](#authentication) below on how to get the authentication tokens. 33 | 34 | ### Environment variables 35 | 36 | Instead of using cli flags you can provide configuration via environment variables with the `MIRRORMAKER_` prefix: 37 | ``` 38 | export MIRRORMAKER_GITHUB_TOKEN xxx 39 | export MIRRORMAKER_GITLAB_TOKEN xxx 40 | 41 | gitlab-mirror-maker 42 | ``` 43 | 44 | ### Dry run 45 | 46 | Run with `--dry-run` flag to only print the summary and don't make any changes. 47 | 48 | ### Full synopsis 49 | 50 | ``` 51 | Usage: gitlab-mirror-maker [OPTIONS] [REPO] 52 | 53 | Set up mirroring of repositories from GitLab to GitHub. 54 | 55 | By default, mirrors for all repositories owned by the user will be set up. 56 | 57 | If the REPO argument is given, a mirror will be set up for that repository 58 | only. REPO can be either a simple project name ("myproject"), in which 59 | case its namespace is assumed to be the current user, or the path of a 60 | project under a specific namespace ("mynamespace/myproject"). 61 | 62 | Options: 63 | --version Show the version and exit. 64 | --github-token TEXT GitHub authentication token [required] 65 | --gitlab-token TEXT GitLab authentication token [required] 66 | --github-user TEXT GitHub username. If not provided, your GitLab 67 | username will be used by default. 68 | 69 | --dry-run / --no-dry-run If enabled, a summary will be printed and no 70 | mirrors will be created. 71 | 72 | --help Show this message and exit. 73 | ``` 74 | 75 | # How it works? 76 | 77 | GitLab Mirror Maker uses the [remote mirrors API](https://docs.gitlab.com/ee/api/remote_mirrors.html) to create [push mirrors](https://docs.gitlab.com/ee/user/project/repository/repository_mirroring.html#pushing-to-a-remote-repository-core) of your GitLab repositories. 78 | 79 | For each public repository in your GitLab account a new GitHub repository is created using the same name and description. It also adds a `[mirror]` suffix at the end of the description and sets the website URL the original GitLab repo. See [the mirror of this repo](https://github.com/grdl/gitlab-mirror-maker) as an example. 80 | 81 | Once the mirror is created it automatically updates the target GitHub repository every time changes are pushed to the original GitLab repo. 82 | 83 | ### What is mirrored? 84 | 85 | Only public repositories are mirrored to avoid publishing something private. 86 | 87 | Only the commits, branches and tags are mirrored. No other repository data such as issues, pull requests, comments, wikis etc. are mirrored. 88 | 89 | 90 | # Authentication 91 | 92 | GitLab Mirror Maker needs authentication tokens for both GitLab and GitHub to be able to create mirrors. 93 | 94 | ### How to get the GitLab token? 95 | 96 | - Click on your GitLab user -> Settings -> Access Tokens 97 | - Pick a name for your token and choose the `api` scope 98 | - Click `Create personal access token` and save it somewhere secure 99 | - Do not share it! It grants full access to your account! 100 | 101 | Here's more information about [GitLab personal tokens](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html). 102 | 103 | ### How to get the GitHub token? 104 | 105 | - Click on your GitHub user -> Settings -> Developer settings -> Personal access tokens -> Generate new token 106 | - Pick a name for your token and choose the `public_repo` scope 107 | - Click `Generate token` and save it somewhere secure 108 | 109 | Here's more information about [GitHub personal tokens](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). 110 | 111 | 112 | # Automate with GitLab CI 113 | 114 | Instead of running the tool manually you may want to schedule it to run periodically with GitLab CI to make sure that any new repositories are automatically mirrored. 115 | 116 | Here's a `.gitlab-ci.yml` snippet you can use: 117 | ```yaml 118 | job: 119 | image: python:3.8-alpine 120 | script: 121 | - pip install gitlab-mirror-maker 122 | - gitlab-mirror-maker 123 | only: 124 | - schedules 125 | 126 | ``` 127 | 128 | Here's more info about creating [scheduled pipelines with GitLab CI](https://docs.gitlab.com/ee/ci/pipelines/schedules.html). 129 | -------------------------------------------------------------------------------- /example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 73 | 96 | 97 | 98 | ~ g gitlab-mirror-maker gitlab-mirror-maker gitlab-mirror-maker gitlab-mirror-maker gitlab-mirror-maker gitlab-mirror-maker gitlab-mirror-maker gitlab-mirror-maker gitlab-mirror-maker gitlab-mirror-maker gitlab-mirror-maker Getting your public GitLab repositories Getting your public GitLab repositoriesGetting your public GitHub repositoriesChecking mirrors status [#######-----------------------------] 20%Checking mirrors status [##########--------------------------] 30%Checking mirrors status [##############----------------------] 40%Checking mirrors status [##################------------------] 50%Checking mirrors status [#####################---------------] 60%Checking mirrors status [#########################-----------] 70%Checking mirrors status [############################--------] 80%Checking mirrors status [################################----] 90%Checking mirrors status [####################################] 100%Your mirrors status summary:GitLab repo GitHub repo Mirror------------------------------- ------------- ---------grdl/calendelight ✔ created ✔ createdgrdl/dotfiles ✔ created ✔ createdgrdl/gitlab-mirror-maker ✔ created ✔ createdgrdl/grafana-dashboards-builder ✔ created ✘ missinggrdl/grdl.dev ✘ missing ✘ missinggrdl/influx-converter ✔ created ✔ createdgrdl/pronestheus ✔ created ✔ createdgrdl/testflux ✔ created ✔ createdgrdl/testsite ✔ created ✔ createdgrdl/zapatero-infra ✔ created ✔ createdCreating mirrors [###---------------------------------] 10%Creating mirrors [#######-----------------------------] 20%Creating mirrors [##########--------------------------] 30%Creating mirrors [####################################] 100%Done!~ took 5s 99 | -------------------------------------------------------------------------------- /mirrormaker/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | 3 | try: 4 | __version__ = version('gitlab-mirror-maker') 5 | except: 6 | __version__ = '0.0.0' 7 | -------------------------------------------------------------------------------- /mirrormaker/github.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import sys 3 | from pprint import pprint 4 | 5 | # GitHub user authentication token 6 | token = '' 7 | 8 | # GitHub username (under this user namespace the mirrors will be created) 9 | user = '' 10 | 11 | 12 | def get_repos(): 13 | """Finds all public GitHub repositories (which are not forks) of authenticated user. 14 | 15 | Returns: 16 | - List of public GitHub repositories. 17 | """ 18 | 19 | url = 'https://api.github.com/user/repos?type=public' 20 | headers = {'Authorization': f'Bearer {token}'} 21 | 22 | repos = [] 23 | try: 24 | while url: 25 | r = requests.get(url, headers=headers) 26 | r.raise_for_status() 27 | repos.extend(r.json()) 28 | # handle pagination 29 | url = r.links.get("next", {}).get("url", None) 30 | except requests.exceptions.RequestException as e: 31 | raise SystemExit(e) 32 | 33 | # Return only non forked repositories 34 | return [x for x in repos if not x['fork']] 35 | 36 | 37 | def repo_exists(github_repos, repo_slug): 38 | """Checks if a repository with a given slug exists among the public GitHub repositories. 39 | 40 | Args: 41 | - github_repos: List of GitHub repositories. 42 | - repo_slug: Repository slug (usually in a form of path with a namespace, eg: "username/reponame"). 43 | 44 | Returns: 45 | - True if repository exists, False otherwise. 46 | """ 47 | 48 | return any(repo['full_name'] == repo_slug for repo in github_repos) 49 | 50 | 51 | def create_repo(gitlab_repo): 52 | """Creates GitHub repository based on a metadata from given GitLab repository. 53 | 54 | Args: 55 | - gitlab_repo: GitLab repository which metadata (ie. name, description etc.) is used to create the GitHub repo. 56 | 57 | Returns: 58 | - JSON representation of created GitHub repo. 59 | """ 60 | 61 | url = 'https://api.github.com/user/repos' 62 | headers = {'Authorization': f'Bearer {token}'} 63 | 64 | data = { 65 | 'name': gitlab_repo['path'], 66 | 'description': f'{gitlab_repo["description"]} [mirror]', 67 | 'homepage': gitlab_repo['web_url'], 68 | 'private': False, 69 | 'has_wiki': False, 70 | 'has_projects': False 71 | } 72 | 73 | try: 74 | r = requests.post(url, json=data, headers=headers) 75 | r.raise_for_status() 76 | except requests.exceptions.RequestException as e: 77 | pprint(e.response.json(), stream=sys.stderr) 78 | raise SystemExit(e) 79 | 80 | return r.json() 81 | -------------------------------------------------------------------------------- /mirrormaker/gitlab.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | # GitLab user authentication token 4 | token = '' 5 | 6 | 7 | def get_repos(): 8 | """Finds all public GitLab repositories of authenticated user. 9 | 10 | Returns: 11 | - List of public GitLab repositories. 12 | """ 13 | 14 | url = 'https://gitlab.com/api/v4/projects?visibility=public&owned=true&archived=false' 15 | headers = {'Authorization': f'Bearer {token}'} 16 | 17 | try: 18 | r = requests.get(url, headers=headers) 19 | r.raise_for_status() 20 | except requests.exceptions.RequestException as e: 21 | raise SystemExit(e) 22 | 23 | return r.json() 24 | 25 | 26 | def get_user(): 27 | url = 'https://gitlab.com/api/v4/user' 28 | headers = {'Authorization': f'Bearer {token}'} 29 | 30 | try: 31 | r = requests.get(url, headers=headers) 32 | r.raise_for_status() 33 | except requests.exceptions.RequestException as e: 34 | raise SystemExit(e) 35 | 36 | return r.json() 37 | 38 | 39 | def get_repo_by_shorthand(shorthand): 40 | if "/" not in shorthand: 41 | user = get_user()["username"] 42 | namespace, project = user, shorthand 43 | else: 44 | namespace, project = shorthand.rsplit("/", maxsplit=1) 45 | 46 | project_id = requests.utils.quote("/".join([namespace, project]), safe="") 47 | 48 | url = f'https://gitlab.com/api/v4/projects/{project_id}' 49 | headers = {'Authorization': f'Bearer {token}'} 50 | 51 | try: 52 | r = requests.get(url, headers=headers) 53 | r.raise_for_status() 54 | except requests.exceptions.RequestException as e: 55 | raise SystemExit(e) 56 | 57 | return r.json() 58 | 59 | 60 | 61 | def get_mirrors(gitlab_repo): 62 | """Finds all configured mirrors of GitLab repository. 63 | 64 | Args: 65 | - gitlab_repo: GitLab repository. 66 | 67 | Returns: 68 | - List of mirrors. 69 | """ 70 | 71 | url = f'https://gitlab.com/api/v4/projects/{gitlab_repo["id"]}/remote_mirrors' 72 | headers = {'Authorization': f'Bearer {token}'} 73 | 74 | try: 75 | r = requests.get(url, headers=headers) 76 | r.raise_for_status() 77 | except requests.exceptions.RequestException as e: 78 | raise SystemExit(e) 79 | 80 | return r.json() 81 | 82 | 83 | def mirror_target_exists(github_repos, mirrors): 84 | """Checks if any of the given mirrors points to any of the public GitHub repositories. 85 | 86 | Args: 87 | - github_repos: List of GitHub repositories. 88 | - mirrors: List of mirrors configured for a single GitLab repository. 89 | 90 | Returns: 91 | - True if any of the mirror points to an existing GitHub repository, False otherwise. 92 | """ 93 | 94 | for mirror in mirrors: 95 | if any(mirror['url'] and mirror['url'].endswith(f'{repo["full_name"]}.git') for repo in github_repos): 96 | return True 97 | 98 | return False 99 | 100 | 101 | def create_mirror(gitlab_repo, github_token, github_user): 102 | """Creates a push mirror of GitLab repository. 103 | 104 | For more details see: 105 | https://docs.gitlab.com/ee/user/project/repository/repository_mirroring.html#pushing-to-a-remote-repository-core 106 | 107 | Args: 108 | - gitlab_repo: GitLab repository to mirror. 109 | - github_token: GitHub authentication token. 110 | - github_user: GitHub username under whose namespace the mirror will be created (defaults to GitLab username if not provided). 111 | 112 | Returns: 113 | - JSON representation of created mirror. 114 | """ 115 | 116 | url = f'https://gitlab.com/api/v4/projects/{gitlab_repo["id"]}/remote_mirrors' 117 | headers = {'Authorization': f'Bearer {token}'} 118 | 119 | # If github-user is not provided use the gitlab username 120 | if not github_user: 121 | github_user = gitlab_repo['owner']['username'] 122 | 123 | data = { 124 | 'url': f'https://{github_user}:{github_token}@github.com/{github_user}/{gitlab_repo["path"]}.git', 125 | 'enabled': True 126 | } 127 | 128 | try: 129 | r = requests.post(url, json=data, headers=headers) 130 | r.raise_for_status() 131 | except requests.exceptions.RequestException as e: 132 | raise SystemExit(e) 133 | 134 | return r.json() 135 | -------------------------------------------------------------------------------- /mirrormaker/mirrormaker.py: -------------------------------------------------------------------------------- 1 | import click 2 | import requests 3 | from tabulate import tabulate 4 | from . import __version__ 5 | from . import gitlab 6 | from . import github 7 | 8 | 9 | @click.command(context_settings={'auto_envvar_prefix': 'MIRRORMAKER'}) 10 | @click.version_option(version=__version__) 11 | @click.option('--github-token', required=True, help='GitHub authentication token') 12 | @click.option('--gitlab-token', required=True, help='GitLab authentication token') 13 | @click.option('--github-user', help='GitHub username. If not provided, your GitLab username will be used by default.') 14 | @click.option('--dry-run/--no-dry-run', default=False, help="If enabled, a summary will be printed and no mirrors will be created.") 15 | @click.argument('repo', required=False) 16 | def mirrormaker(github_token, gitlab_token, github_user, dry_run, repo=None): 17 | """ 18 | Set up mirroring of repositories from GitLab to GitHub. 19 | 20 | By default, mirrors for all repositories owned by the user will be set up. 21 | 22 | If the REPO argument is given, a mirror will be set up for that repository 23 | only. REPO can be either a simple project name ("myproject"), in which case 24 | its namespace is assumed to be the current user, or the path of a project 25 | under a specific namespace ("mynamespace/myproject"). 26 | """ 27 | github.token = github_token 28 | github.user = github_user 29 | gitlab.token = gitlab_token 30 | 31 | if repo: 32 | gitlab_repos = [gitlab.get_repo_by_shorthand(repo)] 33 | else: 34 | click.echo('Getting your public GitLab repositories') 35 | gitlab_repos = gitlab.get_repos() 36 | if not gitlab_repos: 37 | click.echo('There are no public repositories in your GitLab account.') 38 | return 39 | 40 | click.echo('Getting your public GitHub repositories') 41 | github_repos = github.get_repos() 42 | 43 | actions = find_actions_to_perform(gitlab_repos, github_repos) 44 | 45 | print_summary_table(actions) 46 | 47 | perform_actions(actions, dry_run) 48 | 49 | click.echo('Done!') 50 | 51 | 52 | def find_actions_to_perform(gitlab_repos, github_repos): 53 | """Goes over provided repositories and figure out what needs to be done to create missing mirrors. 54 | 55 | Args: 56 | - gitlab_repos: List of GitLab repositories. 57 | - github_repos: List of GitHub repositories. 58 | 59 | Returns: 60 | - actions: List of actions necessary to perform on a GitLab repo to create a mirror 61 | eg: {'gitlab_repo: '', 'create_github': True, 'create_mirror': True} 62 | """ 63 | 64 | actions = [] 65 | with click.progressbar(gitlab_repos, label='Checking mirrors status', show_eta=False) as bar: 66 | for gitlab_repo in bar: 67 | action = check_mirror_status(gitlab_repo, github_repos) 68 | actions.append(action) 69 | 70 | return actions 71 | 72 | 73 | def check_mirror_status(gitlab_repo, github_repos): 74 | """Checks if given GitLab repository has a mirror created among the given GitHub repositories. 75 | 76 | Args: 77 | - gitlab_repo: GitLab repository. 78 | - github_repos: List of GitHub repositories. 79 | 80 | Returns: 81 | - action: Action necessary to perform on a GitLab repo to create a mirror (see find_actions_to_perform()) 82 | """ 83 | 84 | action = {'gitlab_repo': gitlab_repo, 'create_github': True, 'create_mirror': True} 85 | 86 | mirrors = gitlab.get_mirrors(gitlab_repo) 87 | if gitlab.mirror_target_exists(github_repos, mirrors): 88 | action['create_github'] = False 89 | action['create_mirror'] = False 90 | return action 91 | 92 | if github.repo_exists(github_repos, gitlab_repo['path_with_namespace']): 93 | action['create_github'] = False 94 | 95 | return action 96 | 97 | 98 | def print_summary_table(actions): 99 | """Prints a table summarizing whether mirrors are already created or missing 100 | """ 101 | 102 | click.echo('Your mirrors status summary:\n') 103 | 104 | created = click.style(u'\u2714 created', fg='green') 105 | missing = click.style(u'\u2718 missing', fg='red') 106 | 107 | headers = ['GitLab repo', 'GitHub repo', 'Mirror'] 108 | summary = [] 109 | 110 | for action in actions: 111 | row = [action["gitlab_repo"]["path_with_namespace"]] 112 | row.append(missing) if action["create_github"] else row.append(created) 113 | row.append(missing) if action["create_mirror"] else row.append(created) 114 | summary.append(row) 115 | 116 | summary.sort() 117 | 118 | click.echo(tabulate(summary, headers) + '\n') 119 | 120 | 121 | def perform_actions(actions, dry_run): 122 | """Creates GitHub repositories and configures GitLab mirrors where necessary. 123 | 124 | Args: 125 | - actions: List of actions to perform, either creating GitHub repo and/or configuring GitLab mirror. 126 | - dry_run (bool): When True the actions are not performed. 127 | """ 128 | 129 | if dry_run: 130 | click.echo('Run without the --dry-run flag to create missing repositories and mirrors.') 131 | return 132 | 133 | with click.progressbar(actions, label='Creating mirrors', show_eta=False) as bar: 134 | for action in bar: 135 | if action["create_github"]: 136 | github.create_repo(action["gitlab_repo"]) 137 | 138 | if action["create_mirror"]: 139 | gitlab.create_mirror(action["gitlab_repo"], github.token, github.user) 140 | 141 | 142 | if __name__ == '__main__': 143 | # pylint: disable=no-value-for-parameter, unexpected-keyword-arg 144 | mirrormaker() 145 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "An abstract syntax tree for Python with inference support." 4 | name = "astroid" 5 | optional = false 6 | python-versions = ">=3.5" 7 | version = "2.4.1" 8 | 9 | [package.dependencies] 10 | lazy-object-proxy = ">=1.4.0,<1.5.0" 11 | six = ">=1.12,<2.0" 12 | wrapt = ">=1.11,<2.0" 13 | 14 | [[package]] 15 | category = "dev" 16 | description = "Atomic file writes." 17 | marker = "sys_platform == \"win32\"" 18 | name = "atomicwrites" 19 | optional = false 20 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 21 | version = "1.4.0" 22 | 23 | [[package]] 24 | category = "dev" 25 | description = "Classes Without Boilerplate" 26 | name = "attrs" 27 | optional = false 28 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 29 | version = "19.3.0" 30 | 31 | [package.extras] 32 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 33 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 34 | docs = ["sphinx", "zope.interface"] 35 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 36 | 37 | [[package]] 38 | category = "dev" 39 | description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" 40 | name = "autopep8" 41 | optional = false 42 | python-versions = "*" 43 | version = "1.5.2" 44 | 45 | [package.dependencies] 46 | pycodestyle = ">=2.5.0" 47 | 48 | [[package]] 49 | category = "main" 50 | description = "Python package for providing Mozilla's CA Bundle." 51 | name = "certifi" 52 | optional = false 53 | python-versions = "*" 54 | version = "2020.4.5.1" 55 | 56 | [[package]] 57 | category = "main" 58 | description = "Universal encoding detector for Python 2 and 3" 59 | name = "chardet" 60 | optional = false 61 | python-versions = "*" 62 | version = "3.0.4" 63 | 64 | [[package]] 65 | category = "main" 66 | description = "Composable command line interface toolkit" 67 | name = "click" 68 | optional = false 69 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 70 | version = "7.1.2" 71 | 72 | [[package]] 73 | category = "dev" 74 | description = "Cross-platform colored terminal text." 75 | marker = "sys_platform == \"win32\"" 76 | name = "colorama" 77 | optional = false 78 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 79 | version = "0.4.3" 80 | 81 | [[package]] 82 | category = "main" 83 | description = "Internationalized Domain Names in Applications (IDNA)" 84 | name = "idna" 85 | optional = false 86 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 87 | version = "2.9" 88 | 89 | [[package]] 90 | category = "dev" 91 | description = "A Python utility / library to sort Python imports." 92 | name = "isort" 93 | optional = false 94 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 95 | version = "4.3.21" 96 | 97 | [package.extras] 98 | pipfile = ["pipreqs", "requirementslib"] 99 | pyproject = ["toml"] 100 | requirements = ["pipreqs", "pip-api"] 101 | xdg_home = ["appdirs (>=1.4.0)"] 102 | 103 | [[package]] 104 | category = "dev" 105 | description = "A fast and thorough lazy object proxy." 106 | name = "lazy-object-proxy" 107 | optional = false 108 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 109 | version = "1.4.3" 110 | 111 | [[package]] 112 | category = "dev" 113 | description = "McCabe checker, plugin for flake8" 114 | name = "mccabe" 115 | optional = false 116 | python-versions = "*" 117 | version = "0.6.1" 118 | 119 | [[package]] 120 | category = "dev" 121 | description = "More routines for operating on iterables, beyond itertools" 122 | name = "more-itertools" 123 | optional = false 124 | python-versions = ">=3.5" 125 | version = "8.2.0" 126 | 127 | [[package]] 128 | category = "dev" 129 | description = "Core utilities for Python packages" 130 | name = "packaging" 131 | optional = false 132 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 133 | version = "20.3" 134 | 135 | [package.dependencies] 136 | pyparsing = ">=2.0.2" 137 | six = "*" 138 | 139 | [[package]] 140 | category = "dev" 141 | description = "plugin and hook calling mechanisms for python" 142 | name = "pluggy" 143 | optional = false 144 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 145 | version = "0.13.1" 146 | 147 | [package.extras] 148 | dev = ["pre-commit", "tox"] 149 | 150 | [[package]] 151 | category = "dev" 152 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 153 | name = "py" 154 | optional = false 155 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 156 | version = "1.8.1" 157 | 158 | [[package]] 159 | category = "dev" 160 | description = "Python style guide checker" 161 | name = "pycodestyle" 162 | optional = false 163 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 164 | version = "2.5.0" 165 | 166 | [[package]] 167 | category = "dev" 168 | description = "python code static checker" 169 | name = "pylint" 170 | optional = false 171 | python-versions = ">=3.5.*" 172 | version = "2.5.2" 173 | 174 | [package.dependencies] 175 | astroid = ">=2.4.0,<=2.5" 176 | colorama = "*" 177 | isort = ">=4.2.5,<5" 178 | mccabe = ">=0.6,<0.7" 179 | toml = ">=0.7.1" 180 | 181 | [[package]] 182 | category = "dev" 183 | description = "Python parsing module" 184 | name = "pyparsing" 185 | optional = false 186 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 187 | version = "2.4.7" 188 | 189 | [[package]] 190 | category = "dev" 191 | description = "pytest: simple powerful testing with Python" 192 | name = "pytest" 193 | optional = false 194 | python-versions = ">=3.5" 195 | version = "5.4.2" 196 | 197 | [package.dependencies] 198 | atomicwrites = ">=1.0" 199 | attrs = ">=17.4.0" 200 | colorama = "*" 201 | more-itertools = ">=4.0.0" 202 | packaging = "*" 203 | pluggy = ">=0.12,<1.0" 204 | py = ">=1.5.0" 205 | wcwidth = "*" 206 | 207 | [package.extras] 208 | checkqa-mypy = ["mypy (v0.761)"] 209 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 210 | 211 | [[package]] 212 | category = "main" 213 | description = "Python HTTP for Humans." 214 | name = "requests" 215 | optional = false 216 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 217 | version = "2.23.0" 218 | 219 | [package.dependencies] 220 | certifi = ">=2017.4.17" 221 | chardet = ">=3.0.2,<4" 222 | idna = ">=2.5,<3" 223 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 224 | 225 | [package.extras] 226 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 227 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] 228 | 229 | [[package]] 230 | category = "dev" 231 | description = "A utility library for mocking out the `requests` Python library." 232 | name = "responses" 233 | optional = false 234 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 235 | version = "0.10.14" 236 | 237 | [package.dependencies] 238 | requests = ">=2.0" 239 | six = "*" 240 | 241 | [package.extras] 242 | tests = ["coverage (>=3.7.1,<5.0.0)", "pytest-cov", "pytest-localserver", "flake8", "pytest (>=4.6,<5.0)", "pytest"] 243 | 244 | [[package]] 245 | category = "dev" 246 | description = "Python 2 and 3 compatibility utilities" 247 | name = "six" 248 | optional = false 249 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 250 | version = "1.14.0" 251 | 252 | [[package]] 253 | category = "main" 254 | description = "Pretty-print tabular data" 255 | name = "tabulate" 256 | optional = false 257 | python-versions = "*" 258 | version = "0.8.7" 259 | 260 | [package.extras] 261 | widechars = ["wcwidth"] 262 | 263 | [[package]] 264 | category = "dev" 265 | description = "Python Library for Tom's Obvious, Minimal Language" 266 | name = "toml" 267 | optional = false 268 | python-versions = "*" 269 | version = "0.10.0" 270 | 271 | [[package]] 272 | category = "main" 273 | description = "HTTP library with thread-safe connection pooling, file post, and more." 274 | name = "urllib3" 275 | optional = false 276 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 277 | version = "1.25.9" 278 | 279 | [package.extras] 280 | brotli = ["brotlipy (>=0.6.0)"] 281 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] 282 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 283 | 284 | [[package]] 285 | category = "dev" 286 | description = "Measures number of Terminal column cells of wide-character codes" 287 | name = "wcwidth" 288 | optional = false 289 | python-versions = "*" 290 | version = "0.1.9" 291 | 292 | [[package]] 293 | category = "dev" 294 | description = "Module for decorators, wrappers and monkey patching." 295 | name = "wrapt" 296 | optional = false 297 | python-versions = "*" 298 | version = "1.12.1" 299 | 300 | [metadata] 301 | content-hash = "7b57aa347b75629d2515e51d8a14a31cf17f6dc7b794b0eb8587fe1c35c5e21c" 302 | python-versions = "^3.8" 303 | 304 | [metadata.files] 305 | astroid = [ 306 | {file = "astroid-2.4.1-py3-none-any.whl", hash = "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"}, 307 | {file = "astroid-2.4.1.tar.gz", hash = "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1"}, 308 | ] 309 | atomicwrites = [ 310 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 311 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 312 | ] 313 | attrs = [ 314 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 315 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 316 | ] 317 | autopep8 = [ 318 | {file = "autopep8-1.5.2.tar.gz", hash = "sha256:152fd8fe47d02082be86e05001ec23d6f420086db56b17fc883f3f965fb34954"}, 319 | ] 320 | certifi = [ 321 | {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"}, 322 | {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"}, 323 | ] 324 | chardet = [ 325 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 326 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 327 | ] 328 | click = [ 329 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 330 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 331 | ] 332 | colorama = [ 333 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 334 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 335 | ] 336 | idna = [ 337 | {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, 338 | {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, 339 | ] 340 | isort = [ 341 | {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, 342 | {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, 343 | ] 344 | lazy-object-proxy = [ 345 | {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, 346 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, 347 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, 348 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, 349 | {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, 350 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, 351 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, 352 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, 353 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, 354 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, 355 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, 356 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, 357 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, 358 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, 359 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, 360 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, 361 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, 362 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, 363 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, 364 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, 365 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, 366 | ] 367 | mccabe = [ 368 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 369 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 370 | ] 371 | more-itertools = [ 372 | {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, 373 | {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, 374 | ] 375 | packaging = [ 376 | {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, 377 | {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, 378 | ] 379 | pluggy = [ 380 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 381 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 382 | ] 383 | py = [ 384 | {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, 385 | {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, 386 | ] 387 | pycodestyle = [ 388 | {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, 389 | {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, 390 | ] 391 | pylint = [ 392 | {file = "pylint-2.5.2-py3-none-any.whl", hash = "sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b"}, 393 | {file = "pylint-2.5.2.tar.gz", hash = "sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c"}, 394 | ] 395 | pyparsing = [ 396 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 397 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 398 | ] 399 | pytest = [ 400 | {file = "pytest-5.4.2-py3-none-any.whl", hash = "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3"}, 401 | {file = "pytest-5.4.2.tar.gz", hash = "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"}, 402 | ] 403 | requests = [ 404 | {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, 405 | {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, 406 | ] 407 | responses = [ 408 | {file = "responses-0.10.14-py2.py3-none-any.whl", hash = "sha256:3d596d0be06151330cb230a2d630717ab20f7a81f205019481e206eb5db79915"}, 409 | {file = "responses-0.10.14.tar.gz", hash = "sha256:1a78bc010b20a5022a2c0cb76b8ee6dc1e34d887972615ebd725ab9a166a4960"}, 410 | ] 411 | six = [ 412 | {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, 413 | {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, 414 | ] 415 | tabulate = [ 416 | {file = "tabulate-0.8.7-py3-none-any.whl", hash = "sha256:ac64cb76d53b1231d364babcd72abbb16855adac7de6665122f97b593f1eb2ba"}, 417 | {file = "tabulate-0.8.7.tar.gz", hash = "sha256:db2723a20d04bcda8522165c73eea7c300eda74e0ce852d9022e0159d7895007"}, 418 | ] 419 | toml = [ 420 | {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, 421 | {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, 422 | {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, 423 | ] 424 | urllib3 = [ 425 | {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, 426 | {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, 427 | ] 428 | wcwidth = [ 429 | {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, 430 | {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, 431 | ] 432 | wrapt = [ 433 | {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, 434 | ] 435 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "gitlab-mirror-maker" 3 | version = "0.4.1" 4 | description = "Automatically mirror your repositories from GitLab to GitHub" 5 | authors = ["Grzegorz Dlugoszewski "] 6 | maintainers = ["Grzegorz Dlugoszewski "] 7 | readme = "README.md" 8 | license = "MIT" 9 | repository = "https://gitlab.com/grdl/gitlab-mirror-maker" 10 | keywords = ["gitlab", "github"] 11 | 12 | 13 | packages = [ 14 | { include = "mirrormaker" } 15 | ] 16 | 17 | [tool.poetry.scripts] 18 | gitlab-mirror-maker = "mirrormaker.mirrormaker:mirrormaker" 19 | 20 | [tool.poetry.dependencies] 21 | python = "^3.8" 22 | requests = "^2.23.0" 23 | click = "^7.1.1" 24 | tabulate = "^0.8.7" 25 | 26 | [tool.poetry.dev-dependencies] 27 | pylint = "^2.4.4" 28 | autopep8 = "^1.5.1" 29 | responses = "^0.10.12" 30 | pytest = "^5.4.1" 31 | 32 | [build-system] 33 | requires = ["poetry>=0.12"] 34 | build-backend = "poetry.masonry.api" 35 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grdl/gitlab-mirror-maker/0d4be3e3c843d3ec5e36421ccce926c2f7bc1e6d/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_mirrormaker.py: -------------------------------------------------------------------------------- 1 | import responses 2 | import mirrormaker 3 | from mirrormaker import github 4 | from mirrormaker import gitlab 5 | 6 | 7 | @responses.activate 8 | def test_filter_forked_repos(): 9 | resp_json = [{'name': 'repo_1', 'fork': True}, 10 | {'name': 'repo_2', 'fork': False}] 11 | 12 | responses.add(responses.GET, 'https://api.github.com/user/repos?type=public', 13 | json=resp_json, status=200) 14 | 15 | github_repos = github.get_repos() 16 | 17 | assert len(github_repos) == 1 18 | assert github_repos[0]['name'] == 'repo_2' 19 | 20 | 21 | @responses.activate 22 | def test_filter_no_repos(): 23 | responses.add(responses.GET, 'https://api.github.com/user/repos?type=public', 24 | json=[], status=200) 25 | 26 | github_repos = github.get_repos() 27 | 28 | assert len(github_repos) == 0 29 | 30 | 31 | def test_mirror_exists(): 32 | mirrors = [{'url': 'https://*****:*****@github.com/grdl/one.git'}] 33 | github_repos = [{'full_name': 'grdl/one'}, 34 | {'full_name': 'grdl/two'}] 35 | 36 | assert gitlab.mirror_target_exists(github_repos, mirrors) == True 37 | 38 | mirrors = [] 39 | github_repos = [{'full_name': 'grdl/one'}] 40 | 41 | assert gitlab.mirror_target_exists(github_repos, mirrors) == False 42 | 43 | mirrors = [{'url': 'https://*****:*****@github.com/grdl/one.git'}] 44 | github_repos = [{'full_name': 'grdl/two'}] 45 | 46 | assert gitlab.mirror_target_exists(github_repos, mirrors) == False 47 | 48 | mirrors = [] 49 | github_repos = [] 50 | 51 | assert gitlab.mirror_target_exists(github_repos, mirrors) == False 52 | 53 | mirrors = [{'url': 'https://*****:*****@github.com/grdl/one.git'}] 54 | github_repos = [] 55 | 56 | assert gitlab.mirror_target_exists(github_repos, mirrors) == False 57 | 58 | mirrors = [{'url': 'https://*****:*****@github.com/grdl/one.git'}, 59 | {'url': 'https://*****:*****@github.com/grdl/two.git'}] 60 | github_repos = [{'full_name': 'grdl/two'}, 61 | {'full_name': 'grdl/three'}] 62 | 63 | assert gitlab.mirror_target_exists(github_repos, mirrors) == True 64 | 65 | 66 | def test_github_repo_exists(): 67 | github_repos = [{'full_name': 'grdl/one'}, 68 | {'full_name': 'grdl/two'}] 69 | 70 | slug = 'grdl/one' 71 | 72 | assert github.repo_exists(github_repos, slug) == True 73 | 74 | slug = 'grdl/three' 75 | 76 | assert github.repo_exists(github_repos, slug) == False 77 | 78 | assert github.repo_exists([], slug) == False 79 | --------------------------------------------------------------------------------