This commit is contained in:
Jakub Kropáček 2024-05-27 08:55:13 +02:00
parent d006d8284b
commit 60088b679f
19 changed files with 613 additions and 16 deletions

5
.gitignore vendored
View file

@ -31,3 +31,8 @@ dmypy.json
# PyCharm # PyCharm
.idea/ .idea/
# tests poetry install
.tox/
.coverage

View file

@ -1,29 +1,62 @@
import argparse import argparse
from pathlib import Path
from typing import Sequence from typing import Sequence
from many_repos.config import load_config
from many_repos import common 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): 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: def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=( 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` ." "otherwise using one or multiple `--source SOURCE` / `-s SOURCE` ."
) )
) )
_add_specific_args(parser) _add_specific_args(parser)
common.add_common_args(parser) common.add_common_args(parser)
args = parser.parse_args(argv)
args = parser.parse_args(argv)
config = load_config(args.config_filename) 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 return 0
if __name__ == "__main__": if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())

View file

@ -1,9 +1,20 @@
import argparse import argparse
import os import os
from typing import NamedTuple
def add_common_args(parser: argparse.ArgumentParser): def add_common_args(parser: argparse.ArgumentParser):
parser.add_argument( parser.add_argument(
'-C', '--config-filename', '-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`).', help='use a non-default config file (default `%(default)s`).',
) )
class Repository(NamedTuple):
name: str
namespace: str
url: str
fork: bool
vcs: str

View file

@ -1,25 +1,48 @@
import tomllib import tomllib
from pathlib import Path from pathlib import Path
from typing import Any
from typing import NamedTuple from typing import NamedTuple
from typing import Self
from many_repos import errors from many_repos import errors
class Source(NamedTuple): class Source(NamedTuple):
source_type: str source_type: str
username: str username: str
token: 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): class Config(NamedTuple):
output_dir: str | Path 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: def load_config(config_path: str | Path) -> Config:
config_path = Path(config_path) config_path = Path(config_path)
if not config_path.exists(): if not config_path.exists():
raise errors.ConfigNotFound(f"Config at path {config_path} not found!") 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) 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)

View file

@ -1,3 +1,13 @@
class ConfigNotFound(Exception): 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 pass

View file

@ -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!")

View file

@ -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)

View file

@ -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}"
}

View file

@ -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
}

View file

@ -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")

View file

326
poetry.lock generated
View file

@ -1,7 +1,327 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. # 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] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.10"
content-hash = "34e39677d8527182346093002688d17a5d2fc204b9eb3e094b2e6ac519028228" content-hash = "d1b9e19425ac7172708e0c3145dd78bc672c6c47b7a02b6de4251073db7fa713"

View file

@ -14,9 +14,15 @@ packages = [
many-repos-clone = "many_repos.clone:main" many-repos-clone = "many_repos.clone:main"
[tool.poetry.dependencies] [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] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"

0
tests/__init__.py Normal file
View file

3
tests/dummy_test.py Normal file
View file

@ -0,0 +1,3 @@
class TestDummy:
def test_dummy(self):
assert 1 == 1

View file

View file

@ -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
)

View file

@ -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)

32
tox.ini Normal file
View file

@ -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/