diff --git a/.gitignore b/.gitignore index 42b97b6..082793a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,8 @@ dmypy.json # PyCharm .idea/ + +# tests poetry install + +.tox/ +.coverage \ No newline at end of file diff --git a/many_repos/clone.py b/many_repos/clone.py index b105c69..f11da5d 100644 --- a/many_repos/clone.py +++ b/many_repos/clone.py @@ -1,29 +1,62 @@ import argparse - +from pathlib import Path from typing import Sequence -from many_repos.config import load_config from many_repos import common +from many_repos import source_type +from many_repos.config import load_config, Config + def _add_specific_args(parser: argparse.ArgumentParser): - parser.add_argument("-s", "--source", action="append") + parser.add_argument("-s", "--source", help="limit cloning repositores only to these sources", action="append") + + +def _construct_path(repo: common.Repository, config: Config) -> Path: + return (Path(config.output_dir) / repo.vcs / repo.namespace / repo.name).resolve() + + +def _create_path(path: Path): + print(f"Creating `{path}`...") + path.mkdir(parents=True, exist_ok=True) + + +def _get_repositories_to_clone(config: Config, sources: list[str] | None = None) -> list[common.Repository]: + repos = [] + for name, source_config in config.sources_configs.items(): + if sources and name not in sources: + continue + print(f"Getting {name} repositories..") + + source_cls = source_type.get(source_config.source_type) + source = source_cls(source_config) + + all_repos = source.get_repositories() + filtered_repos = source.filter_repositories(all_repos) + repos.extend(filtered_repos) + return repos def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser( description=( - "Clone all repositores into `output_dir` from every source, unless specified " + "Clone all repositories into `output_dir` from every source, unless specified " "otherwise using one or multiple `--source SOURCE` / `-s SOURCE` ." ) ) _add_specific_args(parser) common.add_common_args(parser) - - args = parser.parse_args(argv) + args = parser.parse_args(argv) config = load_config(args.config_filename) + repositories = _get_repositories_to_clone(config, args.source) + + for repository in repositories: + repo_path = _construct_path(repository, config) + if not repo_path.exists(): + _create_path(repo_path) return 0 + if __name__ == "__main__": raise SystemExit(main()) diff --git a/many_repos/common.py b/many_repos/common.py index d57d430..7c5353c 100644 --- a/many_repos/common.py +++ b/many_repos/common.py @@ -1,9 +1,20 @@ import argparse import os +from typing import NamedTuple + + def add_common_args(parser: argparse.ArgumentParser): parser.add_argument( '-C', '--config-filename', - default=os.getenv("MANY_REPOS_CONFIG_PATH") or "repos.toml", + default=os.getenv("MANY_REPOS_CONFIG_FILE") or "repos.toml", help='use a non-default config file (default `%(default)s`).', ) + + +class Repository(NamedTuple): + name: str + namespace: str + url: str + fork: bool + vcs: str diff --git a/many_repos/config.py b/many_repos/config.py index f157755..b5e4b62 100644 --- a/many_repos/config.py +++ b/many_repos/config.py @@ -1,25 +1,48 @@ import tomllib - from pathlib import Path +from typing import Any from typing import NamedTuple +from typing import Self from many_repos import errors + class Source(NamedTuple): source_type: str username: str token: str + forks: bool + + @classmethod + def from_dict(cls, _dict: dict[str, Any]) -> Self: + return cls( + source_type=_dict.get("type"), + username=_dict.get("username"), + token=_dict.get("token"), + forks=_dict.get("forks", False) + ) + class Config(NamedTuple): output_dir: str | Path - sources: dict[str, Source] | None + sources_configs: dict[str, Source] | None + + @classmethod + def from_dict(cls, _dict: dict[str, Any]) -> Self: + output_dir = _dict.get("config").get("output_dir", None) + sources = {name: Source.from_dict(src) for name, src in _dict.get("sources").items()} + if not output_dir: + raise errors.InvalidConfig("`output_dir` needs to be specified!") + cfg = cls(output_dir=output_dir, sources_configs=sources) + return cfg + def load_config(config_path: str | Path) -> Config: config_path = Path(config_path) if not config_path.exists(): raise errors.ConfigNotFound(f"Config at path {config_path} not found!") - with config_path.open() as fp: + with config_path.open("rb") as fp: parsed_config = tomllib.load(fp) - return Config(output_dir="~/repos/", sources={"gitlab": Source(source_type="gitlab", username="justscreamy", token="urgay")}) + return Config.from_dict(parsed_config) diff --git a/many_repos/errors.py b/many_repos/errors.py index 50efe62..5453c30 100644 --- a/many_repos/errors.py +++ b/many_repos/errors.py @@ -1,3 +1,13 @@ class ConfigNotFound(Exception): - "Raised when non-existing config was tried to be used" + """Raised when non-existing config was tried to be used""" + pass + + +class InvalidConfig(Exception): + """Raised when config is invalid""" + pass + + +class InvalidSource(Exception): + """Raised when invalid source is used""" pass diff --git a/many_repos/source_type/__init__.py b/many_repos/source_type/__init__.py new file mode 100644 index 0000000..6f3276d --- /dev/null +++ b/many_repos/source_type/__init__.py @@ -0,0 +1,13 @@ +import importlib + +from . import base +from .. import errors + + +def get(source_name: str) -> type[base.BaseSource]: + try: + module = importlib.import_module(f"many_repos.source_type.{source_name}") + return module.Source + except (ModuleNotFoundError, AttributeError): + raise errors.InvalidSource(f"Source {source_name} not found!") + diff --git a/many_repos/source_type/base.py b/many_repos/source_type/base.py new file mode 100644 index 0000000..06a5782 --- /dev/null +++ b/many_repos/source_type/base.py @@ -0,0 +1,52 @@ +import abc +import json +from urllib import request + +from many_repos import common +from many_repos import config + + +class BaseSource(abc.ABC): + source_config: config.Source + + def __init__(self, source_config: config.Source): + self.source_config = source_config + + # @abc.abstractmethod + # def authenticate(self) -> bool: + # ... + + @abc.abstractmethod + def get_repositories(self) -> list[common.Repository]: + ... + + def filter_repositories(self, repositories: list[common.Repository]) -> list[common.Repository]: + filtered_repositories = [] + for repo in repositories: + if not self.source_config.forks and repo.fork: + continue + filtered_repositories.append(repo) + return filtered_repositories + + @staticmethod + def _make_request( + url: str, method: str = "GET", *, + body: dict | None = None, headers: dict[str, str] | None = None + ) -> list[dict]: + """ + Helper function to create http requests using urllib + """ + if body: + headers["Content-Type"] = "application/json" + data = json.dumps(body).encode('utf-8') + else: + data = None + + req = request.Request( + url=url, + method=method, + headers=headers, + data=data + ) + res = request.urlopen(req) + return json.load(res) diff --git a/many_repos/source_type/github.py b/many_repos/source_type/github.py new file mode 100644 index 0000000..be7bd31 --- /dev/null +++ b/many_repos/source_type/github.py @@ -0,0 +1,31 @@ +from many_repos import common +from many_repos.source_type.base import BaseSource + + +class Source(BaseSource): + api_url = "https://api.github.com/user/repos?per_page=100?affiliation=owner" + + def get_repositories(self) -> list[common.Repository]: + repos_json = self._make_request(self.api_url, headers=self._headers) + repositories = [] + for repo in repos_json: + if not self.source_config.forks and repo.get("fork"): + continue + namespace, name = repo.get("full_name").split("/", 2) + repositories.append( + common.Repository( + name=name, + namespace=namespace, + url=repo.get("ssh_url"), + fork=repo.get("fork"), + vcs="github.com" + ) + ) + return repositories + + @property + def _headers(self): + return { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {self.source_config.token}" + } diff --git a/many_repos/source_type/gitlab.py b/many_repos/source_type/gitlab.py new file mode 100644 index 0000000..ea82406 --- /dev/null +++ b/many_repos/source_type/gitlab.py @@ -0,0 +1,30 @@ +from many_repos import common +from many_repos.source_type.base import BaseSource + + +class Source(BaseSource): + def get_repositories(self) -> list[common.Repository]: + repos_json = self._make_request(self._api_url, headers=self._headers) + repositories = [] + for repo in repos_json: + namespace, name = repo.get("path_with_namespace").split("/") + repositories.append( + common.Repository( + name=name, + namespace=namespace, + url=repo.get("ssh_url_to_repo"), + fork=bool(repo.get("forked_from_project")), + vcs="gitlab.com" + ) + ) + return repositories + + @property + def _api_url(self) -> str: + return f"https://gitlab.com/api/v4/users/{self.source_config.username}/projects" + + @property + def _headers(self) -> dict[str, str]: + return { + "PRIVATE-TOKEN": self.source_config.token + } diff --git a/many_repos/source_type/gitlab_group.py b/many_repos/source_type/gitlab_group.py new file mode 100644 index 0000000..b3519a9 --- /dev/null +++ b/many_repos/source_type/gitlab_group.py @@ -0,0 +1,8 @@ +from many_repos.source_type.gitlab import Source as BaseSource + + +class Source(BaseSource): + @property + def _api_url(self) -> str: + return (f"https://gitlab.com/api/v4/groups/" + f"{'%2F'.join(self.source_config.username.split('/'))}/projects") diff --git a/many_repos/source_type/utils.py b/many_repos/source_type/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/poetry.lock b/poetry.lock index 1034779..658dfff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,7 +1,327 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. -package = [] + +[[package]] +name = "cachetools" +version = "5.3.3" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, + {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, +] + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.14.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, + {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mypy" +version = "1.10.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pyproject-api" +version = "1.6.1" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"}, + {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"}, +] + +[package.dependencies] +packaging = ">=23.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"] + +[[package]] +name = "pytest" +version = "8.2.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, + {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "ruff" +version = "0.4.5" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"}, + {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"}, + {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"}, + {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"}, + {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"}, + {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"}, + {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"}, + {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tox" +version = "4.15.0" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tox-4.15.0-py3-none-any.whl", hash = "sha256:300055f335d855b2ab1b12c5802de7f62a36d4fd53f30bd2835f6a201dda46ea"}, + {file = "tox-4.15.0.tar.gz", hash = "sha256:7a0beeef166fbe566f54f795b4906c31b428eddafc0102ac00d20998dd1933f6"}, +] + +[package.dependencies] +cachetools = ">=5.3.2" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.13.1" +packaging = ">=23.2" +platformdirs = ">=4.1" +pluggy = ">=1.3" +pyproject-api = ">=1.6.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +virtualenv = ">=20.25" + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"] +testing = ["build[virtualenv] (>=1.0.3)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=8.0.2)", "distlib (>=0.3.8)", "flaky (>=3.7)", "hatch-vcs (>=0.4)", "hatchling (>=1.21)", "psutil (>=5.9.7)", "pytest (>=7.4.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-xdist (>=3.5)", "re-assert (>=1.1)", "time-machine (>=2.13)", "wheel (>=0.42)"] + +[[package]] +name = "typing-extensions" +version = "4.12.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, + {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, +] + +[[package]] +name = "virtualenv" +version = "20.26.2" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, + {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [metadata] lock-version = "2.0" -python-versions = "^3.12" -content-hash = "34e39677d8527182346093002688d17a5d2fc204b9eb3e094b2e6ac519028228" +python-versions = "^3.10" +content-hash = "d1b9e19425ac7172708e0c3145dd78bc672c6c47b7a02b6de4251073db7fa713" diff --git a/pyproject.toml b/pyproject.toml index 737d05e..ea3892c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,15 @@ packages = [ many-repos-clone = "many_repos.clone:main" [tool.poetry.dependencies] -python = "^3.12" +python = "^3.10" +[tool.poetry.group.dev.dependencies] +tox = "^4.15.0" +pytest = "^8.2.1" +ruff = "^0.4.5" +mypy = "^1.10.0" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy_test.py b/tests/dummy_test.py new file mode 100644 index 0000000..158613b --- /dev/null +++ b/tests/dummy_test.py @@ -0,0 +1,3 @@ +class TestDummy: + def test_dummy(self): + assert 1 == 1 diff --git a/tests/source_type/__init__.py b/tests/source_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/source_type/conftest.py b/tests/source_type/conftest.py new file mode 100644 index 0000000..e87ba85 --- /dev/null +++ b/tests/source_type/conftest.py @@ -0,0 +1,13 @@ +import pytest + +from many_repos.config import Source + + +@pytest.fixture(params=[True, False]) +def source_config(forks): + return Source( + source_type="github", + username="JustScreaMy", + token="testToken", + forks=forks + ) diff --git a/tests/source_type/github_test.py b/tests/source_type/github_test.py new file mode 100644 index 0000000..b0ace34 --- /dev/null +++ b/tests/source_type/github_test.py @@ -0,0 +1,7 @@ +from many_repos.source_type.github import Source as GithubSource +from unittest import mock + + +class TestGithubSource: + def test_api_url_generation(self, source_config, monkeypatch): + src = GithubSource(source_config) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..991e78e --- /dev/null +++ b/tox.ini @@ -0,0 +1,32 @@ +[tox] +requires = + tox>=4 +env_list = + py{310,311,312} + lint + coverage + type + +[testenv] +description = run the tests with pytest +deps = pytest +commands = + pytest {posargs:tests} + +[testenv:type] +description = run type checks +deps = mypy +commands = + mypy {posargs:many_repos tests} + +[testenv:lint] +description = run linter +deps = ruff +commands = ruff check {posargs:many_repos tests} + +[testenv:coverage] +description = run coverage report +deps = + pytest + pytest-cov +commands = pytest --cov=many_repos tests/ \ No newline at end of file