PK!&Ә;;LICENSEMIT License Copyright (c) 2019 Benedikt Maximilian Wiberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!reliesl/__init__.pyPK!))Ireliesl/cli/__init__.pyimport click from reliesl.cli.bump import bump from reliesl.cli.release import release @click.group() def reliesl(): pass reliesl.add_command(bump) reliesl.add_command(release) def main(): """Entry point to be used when building reliesl as a library.""" reliesl() PK!::N!N!reliesl/cli/bump.pyimport datetime import os import re from pathlib import Path from typing import Dict from typing import List from typing import Tuple import click import semver import toml CHANGELOG = Path("CHANGELOG.md") PYPROJECT = Path("pyproject.toml") PYPROJECT_BUMP_CONFIG = (PYPROJECT, '(version = ")(.*)(")') EMPTY_PATCH = { "version": "UNRELEASED", "date": "YYYY-MM-DD\n", "release_notes": "\n### Added\n### Fixed\n### Changed\n\n", } RELEASE_CHOICES = { "1": { "description": "MAJOR version when you make incompatible API changes", "function": semver.bump_major, }, "2": { "description": "MINOR version when you add functionality in a backwards compatible manner", "function": semver.bump_minor, }, "3": { "description": "PATCH version when you make backwards compatible bug fixes", "function": semver.bump_patch, }, } PRERELEASE_CHOICES = { "4": { "description": "RELEASE version when you want to release this version", "function": semver.finalize_version, }, "5": { "description": "PRERELEASE version when you want to add a new prerelease", "function": semver.bump_prerelease, }, } # Type Definitions Releasenotes = Dict[str, str] Changelog = List[Releasenotes] def _get_date() -> str: return datetime.date.today().strftime("%Y-%m-%d") def load_changelog(changelog_file: Path = CHANGELOG) -> Changelog: """ Args: changelog_file: path to changelog in keepachangelog.com format Returns: List of releasenote dicts with the following fields: - version: Version number of the patch - date: Date of the patch - release_notes: Markdown formatted release_notes """ with changelog_file.open() as file: changelog = [] current_version = None for line in file: if line.startswith("## "): # Guarantees that only valid release_notes are added if current_version: changelog += [current_version] parts = line.split(" ") patch_version = parts[1] try: # Additional check in case no date is given and as such can not be parsed patch_date = parts[3] except IndexError: patch_date = "YYYY-MM-DD" current_version = { "version": patch_version, "date": patch_date, "release_notes": "", } else: if current_version: current_version["release_notes"] += line changelog += [current_version] return changelog def _format_release_notes(release_notes: Releasenotes) -> str: return f"## {release_notes['version']} - {release_notes['date']}{release_notes['release_notes']}" def save_changelog(changelog: Changelog, filename: Path): """ Args: changelog: Changelog in the same format as returned by load_changelog() filename: filepath under which the changelog will be saved """ click.echo("Saving changelog...") tmpfile = filename.with_suffix(".tmp") with tmpfile.open("w+") as file: for release in changelog: file.write(_format_release_notes(release)) tmpfile.rename(filename) def change_version_in_files( additional_file_edits: List[Tuple[Path, str]], version: str, dry_run: bool = False ): """ Args: additional_file_edits: List of tuples containing a filepath and a regexpr version: Changed Version dry_run: Flag to check if no edits should be made. Helpful when debugging the regular expression. """ for file, regexp in additional_file_edits: # Use tmpfile in case of crash, will replace original later # Careful when using, you need to guarantee that the whole line gets matched click.echo(f"Changes in {file} :") tmp_file = file.with_suffix(".tmp") with file.open("r") as realfile, tmp_file.open("w+") as tmpfile: if dry_run: click.echo(f"{regexp} finds: ") regexp = re.compile(regexp) for line in realfile: re.compile(regexp) line_match = regexp.match(line) if line_match: click.echo(f"\t{line[:-1]} -> ", nl=False) matches = list(line_match.groups()) matches[1] = version line = "".join(matches) + "\n" click.echo(line) tmpfile.write(line) if dry_run: tmp_file.unlink() else: tmp_file.rename(file) def ask_patchtype(project_current_version: str): click.echo(f"The current version is: {project_current_version}") click.echo("What kind of update do you want to make?") if semver.parse(project_current_version)["prerelease"]: RELEASE_CHOICES.update(PRERELEASE_CHOICES) for key, value in RELEASE_CHOICES.items(): click.echo( f"({key}) {value['function'](project_current_version)}\t - {value['description']}" ) click.echo("For more information visit https://semver.org/") patch_type = click.prompt("> ", type=click.Choice(RELEASE_CHOICES.keys())) patch_type = RELEASE_CHOICES[patch_type] return patch_type["function"](project_current_version) def _show_release_notes(release_notes: Releasenotes): click.echo(_format_release_notes(release_notes)) click.echo("-----") click.confirm( "This is how the new release_notes will look like. Do you want to continue?", abort=True, ) def get_version_and_name(pyproject_file: Path = PYPROJECT): with pyproject_file.open() as file: pyproject = toml.load(file) project_pyproject = pyproject["tool"]["poetry"] project_current_version = project_pyproject["version"] project_name = project_pyproject["name"] return project_current_version, project_name def _parse_file_edits(ctx, params, value): return [(Path(file_regex[0]), file_regex[1]) for file_regex in value] @click.command(help="Bump the version number for your project.") @click.option( "--project-dir", default=Path.cwd(), callback=lambda ctx, params, value: Path(value), help="Directory in which to look for the CHANGELOG.md and pyproject.toml", ) @click.option( "--additional-file-edits", "-f", nargs=2, multiple=True, callback=_parse_file_edits, help="Specify for additional files and a corresponding regex where " "the version number needs to be bumped.", ) @click.option( "--dry-run", is_flag=True, default=False, help="Do a dry run to check if file edits work correctly without changing any files.", ) @click.option( "--prerelease", is_flag=True, default=False, help="Change new version to a prerelease.", ) def bump( project_dir: Path, additional_file_edits: List[str], dry_run: bool, prerelease: bool ): additional_file_edits.append(PYPROJECT_BUMP_CONFIG) # Change to the projects directory in a revertible way prevdir = Path.cwd() os.chdir(project_dir.resolve()) try: if dry_run: click.echo("It's a dry run.") click.echo("The following file edits would be made:") change_version_in_files( additional_file_edits, version="", dry_run=True ) click.echo( "\nP.S. Be careful to capture the whole line, not captured parts will be deleted!" ) exit(0) project_current_version, project_name = get_version_and_name() new_version = ask_patchtype(project_current_version) if prerelease: new_version = semver.bump_prerelease(new_version) changelog = load_changelog(CHANGELOG) if changelog[0]["version"] != "UNRELEASED": raise SystemExit( "No new release_notes detected. (Start with '## UNRELEASED')" ) changelog[0]["version"] = new_version changelog[0]["date"] = _get_date() + "\n" _show_release_notes(changelog[0]) # Start of all permanent changes changelog.insert(0, EMPTY_PATCH) save_changelog(changelog, CHANGELOG) # Bump in all other files change_version_in_files(additional_file_edits, version=new_version) finally: os.chdir(prevdir) PK!.greliesl/cli/release.pyimport os from pathlib import Path from typing import Dict from typing import List from typing import Union from urllib.parse import urljoin import click import requests import semver from reliesl.cli.bump import get_version_and_name from reliesl.cli.bump import load_changelog # Type Definitions Header = Dict[str, str] Asset = Dict[str, str] Json = object def get_project_data(project_api_url: str, auth: Header) -> Json: project_data = requests.get(project_api_url, headers=auth) project_data.raise_for_status() return project_data.json() def upload_file(path: Path, project_api_url: str, auth: Header) -> Json: uploads_url = project_api_url + "/uploads" file = {"file": (path.name, path.open("rb"), "application/zip")} result = requests.post(uploads_url, files=file, headers=auth) result.raise_for_status() return result.json() def release_version( semver: str, release_description: str, assets: Union[Asset, List[Asset]], project_api_url: str, auth: Header, ref: str = "master", ) -> Json: if not isinstance(assets, list): assets = [assets] release_url = project_api_url + "/releases" release_name = "v" + semver release_data = { "name": release_name, "tag_name": release_name, "ref": ref, "description": release_description, "assets": {"links": assets}, } auth["Content-Type"] = "application/json" result = requests.post(release_url, json=release_data, headers=auth) result.raise_for_status() return result.json() def upload_wheel( wheel_path: Path, project_api_url: str, project_url: str, auth: Header ) -> Asset: wheel_reply = upload_file( path=wheel_path, project_api_url=project_api_url, auth=auth ) wheel_asset = {"name": wheel_reply["alt"], "url": project_url + wheel_reply["url"]} return wheel_asset def find_wheel(project_name: str, version: str) -> Path: click.echo( f"Trying to find wheel... - {project_name}-{semver.finalize_version(version)}*.whl" ) wheel_path = list( Path("dist/").glob(f"{project_name}-{semver.finalize_version(version)}*.whl") ) if len(wheel_path) != 1: raise SystemExit( f"Wheel could either not be found or is ambiguous: {wheel_path}" ) wheel_path = wheel_path[0] return wheel_path def get_project_url(project_api_url: str, auth: Header) -> str: try: project_data = get_project_data(project_api_url=project_api_url, auth=auth) except requests.exceptions.HTTPError: raise SystemExit("Could not connect to gitlab api. Is the given token correct?") project_url = project_data["web_url"] return project_url def get_commit(branch: str, project_api_url: str, auth: Header) -> str: result = requests.get( project_api_url + f"/repository/branches/{branch}", headers=auth ) result.raise_for_status() return result.json()["commit"]["short_id"] def check_for_version_conflict(version: str, project_api_url: str, auth: Header): result = requests.get(project_api_url + f"/releases", headers=auth) result.raise_for_status() result = result.json() if result: current_version = result[0]["tag_name"] if semver.compare(version, current_version[1:]) != 1: raise SystemExit( f"Release version v{version} is not newer than current version {current_version}!" ) def _get_release_notes() -> str: changelog = load_changelog() # Only interested in newest release notes newest_release = 0 try: # Check if Release 0 is not an unreleased version semver.parse(changelog[newest_release]["version"]) except ValueError: newest_release = 1 patch = changelog[newest_release] return patch["release_notes"] def _parse_project_url(ctx, param, value: str) -> str: if not value: value = "gitlab.com" if not value.endswith("/"): value += "/" if not value.startswith("http"): value = "https://" + value return value @click.command(help="Release a new version of your project on GitLab.") @click.option( "--project-dir", envvar="CI_PROJECT_DIR", type=click.Path(exists=True), help="Root directory of the project you want to release.", ) @click.option( "--project-id", envvar="CI_PROJECT_ID", help="Unique GitLab ID for projects, available in Project Settings > General.", ) @click.option( "--project-url", callback=_parse_project_url, envvar="CI_PROJECT_URL", help="URL of GitLab server.", ) @click.option( "--pypi-release", type=click.Choice(["pypi", "testpypi"]), help="Release to PyPi" ) def release(project_dir: str, project_id: str, project_url: str, pypi_release: str): project_dir = Path(project_dir) branch = os.environ.get("CI_COMMIT_REF_NAME") private_token = os.environ.get("PRIVATE_TOKEN") if not private_token: raise SystemExit("No private token given. Set $PRIVATE_TOKEN") auth = {"PRIVATE-TOKEN": private_token} if not branch: raise SystemExit("No branch specified. Set $CI_COMMIT_REF_NAME") if pypi_release: env_username = pypi_release.upper() + "_USERNAME" env_password = pypi_release.upper() + "_PASSWORD" username = os.environ.get(env_username) password = os.environ.get(env_password) if not (username and password): raise SystemExit( f"Try to publish to {pypi_release}, but environment variable {env_username} or {env_password} not set!" ) # Change to the projects directory in a revertible way prevdir = Path.cwd() os.chdir(project_dir.resolve()) try: version, name = get_version_and_name() project_api_url = urljoin(project_url, f"/api/v4/projects/{project_id}") project_url = get_project_url(project_api_url, auth) check_for_version_conflict(version, project_api_url, auth) commit = get_commit(branch, project_api_url, auth) click.echo(f'Release v{version} on branch "{branch}", commit "{commit}"') os.system(f"poetry build") wheel_path = find_wheel(name, version) wheel_asset = upload_wheel( wheel_path=wheel_path, project_api_url=project_api_url, auth=auth, project_url=project_url, ) release_version( semver=version, release_description=_get_release_notes(), assets=wheel_asset, project_api_url=project_api_url, ref=branch, auth=auth, ) if pypi_release: if pypi_release == "testpypi": os.system( f"poetry publish -r {pypi_release} -u {username} -p {password}" ) else: os.system(f"poetry publish -u {username} -p {password}") finally: os.chdir(prevdir) if __name__ == "__main__": # Helpful for debugging release(["--pypi-release", "testpypi"]) PK!Hj(F(,(reliesl-0.1.1.dist-info/entry_points.txtN+I/N.,()*JL-αz9Vy\\PK!&Ә;;reliesl-0.1.1.dist-info/LICENSEMIT License Copyright (c) 2019 Benedikt Maximilian Wiberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTUreliesl-0.1.1.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H;ؿ  reliesl-0.1.1.dist-info/METADATAXao8_ޡ"t$8oAXkZ%֒%);ߛ$i#̛7þW^fNz"NѭDXUjhX:N^b*iqօvB oL) zSISگS+U{:]JӾ qqDJōqb}&qΫIjqV6k3nEqoԍNU,Mm7fn"Σ^׭iݑ- 3u4m}aDjBټ{Jr"z{grU8(szT_kmMnO&Δ+Zp-McZeN_m>Tx#l$WU>yR;?)p^7x\rޑIrrrt;RNUke UJ@fa`1N{cG%C.Kn(1D<6H9̦FRGRV-+pTW96 )(Mw B2s6r5N'Ǽ-]1Ρ},aʸ9jB[u6ov;ΨnN3oĶ#wc>/} 6:qtC;>ݴ/J2Ger+U"~nsEH,PّX*[(YFpaa\ci|@fҖLeNkBIy3-jJ0K_P?dpS"#!R,%1(VDi HZkAɈu S5!^$bVakzй3 ɓJnJ5`5 |Կc(9G0RYVP-R<Dna@U*Yb˒H&:>CuCIؕR_n>KlA3#2\3.J'sZ%W_WmJCタ1'Ќ T09=T\'zt-<JWގ2 H6BdOĴt,p#%E! ĔIz l 2iqHTp! ?rR( J]G,A >f|J <=ϑ d(5B$.Y˲U$ƍVVǥm&ÆЈw⵴Z"Nw<>\N_[xy7}xFZ1]=̞ɲ~'Ps=:m c[xm+͔ x)@gF,0"~->$"BmPpHUn3t`S 5NKNV b]aSR|xi滯˕hz/>_\MYEUbǓ5i?}eXf1%8qJn/m>RmZc:Q4.R^v}f(@RT3f˥ιӬ$s*&Uq)=Srt>?R^DI"zN?/LGa__{Drf W}W-" ݒi`? ^E)kdJJ0VӶDH;GZ2ެg?L풰'16cf͘J _#.%q];OűB\`7TGуPX$Z!wWP/YѬc$ q-FW58D,LXd) At 26 ?'q8u= &lwD'XhiChĨǽX|20cOѾ FՓ;]kz @B{vOaQ3c0a8><x n0ه߮n#u @kѠ1TfՏMԋĊos!n.T8}Ϥi9#nKfiș(LDAu39Qr@Q>vL>v{Q?ܒ{z;0pp1dF!t]=$Og{wfI 1lW<5#,KI{2kϼzrUqY(]na61L`pA.e"Q&xsCIEip7~!F23?(z"9|زn㹀فR݃5F3~ƊIJiBit _@eޔ5CQwO)3H!5pYC6$='~ׯ޾bmbŸfY@ZPK!H7xreliesl-0.1.1.dist-info/RECORDѻ@|. HJ/ -PQҠӯ8֌INSo3r!Cϩ ɿB}okPHˡ*L*VhhSf&ٮscJ3IA]iE:ϧn&VZ̲~9VVb]<-}A |{}i6KK}هdyRB_nѶ^\hftu߀]Yu3g)^6FL smEwkƒřH'r#1D;cB=zxdcP_ǍnM$R@i#7j)65ex˛on0|X&+ *+0־L53A5­xdִ7&}?185' ƥ LteX|g1PK!&Ә;;LICENSEPK!`reliesl/__init__.pyPK!))Ireliesl/cli/__init__.pyPK!::N!N!reliesl/cli/bump.pyPK!.g`'reliesl/cli/release.pyPK!Hj(F(,(1Creliesl-0.1.1.dist-info/entry_points.txtPK!&Ә;;Creliesl-0.1.1.dist-info/LICENSEPK!HڽTUHreliesl-0.1.1.dist-info/WHEELPK!H;ؿ  Hreliesl-0.1.1.dist-info/METADATAPK!H7xRreliesl-0.1.1.dist-info/RECORDPK S