├── .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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------