autopub 0.3.0__py3-none-any.whl → 1.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
autopub/__init__.py CHANGED
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from collections.abc import Iterable
6
+ from pathlib import Path
7
+
8
+ import frontmatter
9
+
10
+ from autopub.exceptions import (
11
+ ArtifactHashMismatch,
12
+ ArtifactNotFound,
13
+ AutopubException,
14
+ NoPackageManagerPluginFound,
15
+ ReleaseFileEmpty,
16
+ ReleaseFileNotFound,
17
+ ReleaseNotesEmpty,
18
+ ReleaseTypeInvalid,
19
+ ReleaseTypeMissing,
20
+ )
21
+ from autopub.plugins import AutopubPackageManagerPlugin, AutopubPlugin
22
+ from autopub.types import ReleaseInfo
23
+
24
+
25
+ class Autopub:
26
+ RELEASE_FILE_PATH = "RELEASE.md"
27
+
28
+ def __init__(self, plugins: Iterable[type[AutopubPlugin]] = ()) -> None:
29
+ self.plugins = [plugin_class() for plugin_class in plugins]
30
+
31
+ @property
32
+ def release_file(self) -> Path:
33
+ return Path.cwd() / self.RELEASE_FILE_PATH
34
+
35
+ @property
36
+ def release_notes(self) -> str:
37
+ return self.release_file.read_text()
38
+
39
+ @property
40
+ def release_file_hash(self) -> str:
41
+ return hashlib.sha256(self.release_notes.encode("utf-8")).hexdigest()
42
+
43
+ @property
44
+ def release_data_file(self) -> Path:
45
+ return Path(".autopub") / "release_data.json"
46
+
47
+ # TODO: typed dict
48
+ @property
49
+ def release_data(self) -> ReleaseInfo:
50
+ if not self.release_data_file.exists():
51
+ raise ArtifactNotFound()
52
+
53
+ release_data = json.loads(self.release_data_file.read_text())
54
+
55
+ if release_data["hash"] != self.release_file_hash:
56
+ raise ArtifactHashMismatch()
57
+
58
+ return ReleaseInfo(
59
+ release_type=release_data["release_type"],
60
+ release_notes=release_data["release_notes"],
61
+ additional_info=release_data["plugin_data"],
62
+ )
63
+
64
+ def check(self) -> ReleaseInfo:
65
+ release_file = Path(self.RELEASE_FILE_PATH)
66
+
67
+ if not release_file.exists():
68
+ raise ReleaseFileNotFound()
69
+
70
+ try:
71
+ release_info = self._validate_release_notes(self.release_notes)
72
+ except AutopubException as e:
73
+ for plugin in self.plugins:
74
+ plugin.on_release_notes_invalid(e)
75
+ raise
76
+
77
+ for plugin in self.plugins:
78
+ plugin.on_release_notes_valid(release_info)
79
+
80
+ self._write_artifact(release_info)
81
+
82
+ return release_info
83
+
84
+ def build(self) -> None:
85
+ if not any(
86
+ isinstance(plugin, AutopubPackageManagerPlugin) for plugin in self.plugins
87
+ ):
88
+ raise NoPackageManagerPluginFound()
89
+
90
+ for plugin in self.plugins:
91
+ if isinstance(plugin, AutopubPackageManagerPlugin):
92
+ plugin.build()
93
+
94
+ def prepare(self) -> None:
95
+ for plugin in self.plugins:
96
+ plugin.prepare(self.release_data)
97
+
98
+ def publish(self, repository: str | None = None) -> None:
99
+ # TODO: shall we put this in a function, to make it
100
+ # clear that we are triggering the logic to check the release file?
101
+ self.release_data
102
+
103
+ for plugin in self.plugins:
104
+ if isinstance(plugin, AutopubPackageManagerPlugin):
105
+ plugin.publish(repository=repository)
106
+
107
+ def _write_artifact(self, release_info: ReleaseInfo) -> None:
108
+ data = {
109
+ "hash": self.release_file_hash,
110
+ "release_type": release_info.release_type,
111
+ "release_notes": release_info.release_notes,
112
+ "plugin_data": {
113
+ key: value
114
+ for plugin in self.plugins
115
+ for key, value in plugin.data.items()
116
+ },
117
+ }
118
+
119
+ self.release_data_file.parent.mkdir(exist_ok=True)
120
+ self.release_data_file.write_text(json.dumps(data))
121
+
122
+ def _deprecated_load(self, release_notes: str) -> ReleaseInfo:
123
+ # supports loading of old release notes format, which is
124
+ # deprecated and will be removed in a future release
125
+ # and looks like this:
126
+ # Release type: patch
127
+ # release notes here.
128
+
129
+ try:
130
+ release_info, release_notes = release_notes.split("\n", 1)
131
+ except ValueError as e:
132
+ raise ReleaseTypeMissing() from e
133
+
134
+ release_info = release_info.lower()
135
+
136
+ if not release_info.startswith("release type:"):
137
+ raise ReleaseTypeMissing()
138
+
139
+ release_type = release_info.split(":", 1)[1].strip().lower()
140
+
141
+ return ReleaseInfo(
142
+ release_type=release_type, release_notes=release_notes.strip()
143
+ )
144
+
145
+ def _load_from_frontmatter(self, release_notes: str) -> ReleaseInfo:
146
+ # supports loading of new release notes format, which looks like this:
147
+ # ---
148
+ # release type: patch
149
+ # ---
150
+ # release notes here.
151
+
152
+ post = frontmatter.loads(release_notes)
153
+
154
+ data: dict[str, str] = post.to_dict()
155
+
156
+ release_type = data.pop("release type").lower()
157
+
158
+ if release_type not in ("major", "minor", "patch"):
159
+ raise ReleaseTypeInvalid(release_type)
160
+
161
+ if post.content.strip() == "":
162
+ raise ReleaseNotesEmpty()
163
+
164
+ return ReleaseInfo(
165
+ release_type=release_type,
166
+ release_notes=post.content,
167
+ additional_info=data,
168
+ )
169
+
170
+ def _validate_release_notes(self, release_notes: str) -> ReleaseInfo:
171
+ if not release_notes:
172
+ raise ReleaseFileEmpty()
173
+
174
+ try:
175
+ release_info = self._load_from_frontmatter(release_notes)
176
+ except KeyError:
177
+ release_info = self._deprecated_load(release_notes)
178
+
179
+ for plugin in self.plugins:
180
+ plugin.validate_release_notes(release_info)
181
+
182
+ return release_info
@@ -0,0 +1,121 @@
1
+ from typing import Optional, TypedDict
2
+
3
+ import rich
4
+ import typer
5
+ from rich.console import Group
6
+ from rich.markdown import Markdown
7
+ from rich.padding import Padding
8
+ from rich.panel import Panel
9
+ from typing_extensions import Annotated
10
+
11
+ from autopub import Autopub
12
+ from autopub.cli.plugins import find_plugins
13
+ from autopub.exceptions import AutopubException
14
+
15
+ app = typer.Typer()
16
+
17
+
18
+ class State(TypedDict):
19
+ plugins: list[str]
20
+
21
+
22
+ state: State = {"plugins": []}
23
+
24
+
25
+ @app.command()
26
+ def check():
27
+ """This commands checks if the current PR has a valid release file."""
28
+
29
+ autopub = Autopub(plugins=find_plugins(state["plugins"]))
30
+
31
+ try:
32
+ release_info = autopub.check()
33
+ except AutopubException as e:
34
+ rich.print(Panel.fit(f"[red]{e.message}"))
35
+
36
+ raise typer.Exit(1) from e
37
+ else:
38
+ rich.print(
39
+ Padding(
40
+ Group(
41
+ (
42
+ "[bold on bright_magenta] Release type: [/] "
43
+ f"[yellow italic underline]{release_info.release_type}[/]\n"
44
+ ),
45
+ "[bold on bright_magenta] Release notes: [/]\n",
46
+ Markdown(release_info.release_notes),
47
+ "\n---\n\n[green bold]Release file is valid![/] 🚀",
48
+ ),
49
+ (1, 1),
50
+ )
51
+ )
52
+
53
+
54
+ @app.command()
55
+ def build():
56
+ autopub = Autopub(plugins=find_plugins(state["plugins"]))
57
+
58
+ try:
59
+ autopub.build()
60
+ except AutopubException as e:
61
+ rich.print(Panel.fit(f"[red]{e.message}"))
62
+
63
+ raise typer.Exit(1) from e
64
+ else:
65
+ rich.print(Panel.fit("[green]Build succeeded"))
66
+
67
+
68
+ @app.command()
69
+ def prepare():
70
+ autopub = Autopub(plugins=find_plugins(state["plugins"]))
71
+
72
+ try:
73
+ autopub.prepare()
74
+ except AutopubException as e:
75
+ rich.print(Panel.fit(f"[red]{e.message}"))
76
+
77
+ raise typer.Exit(1) from e
78
+ else:
79
+ rich.print(Panel.fit("[green]Preparation succeeded"))
80
+
81
+
82
+ @app.command()
83
+ def publish(
84
+ repository: Annotated[
85
+ Optional[str],
86
+ typer.Option("--repository", "-r", help="Repository to publish to"),
87
+ ] = None,
88
+ ):
89
+ autopub = Autopub(plugins=find_plugins(state["plugins"]))
90
+
91
+ try:
92
+ autopub.publish(repository=repository)
93
+ except AutopubException as e:
94
+ rich.print(Panel.fit(f"[red]{e.message}"))
95
+
96
+ raise typer.Exit(1) from e
97
+ else:
98
+ rich.print(Panel.fit("[green]Publishing succeeded"))
99
+
100
+
101
+ @app.callback(invoke_without_command=True)
102
+ def main(
103
+ plugins: list[str] = typer.Option(
104
+ [],
105
+ "--plugin",
106
+ "-p",
107
+ help="List of plugins to use",
108
+ ),
109
+ should_show_version: Annotated[
110
+ Optional[bool], typer.Option("--version", is_eager=True)
111
+ ] = None,
112
+ ):
113
+ state["plugins"] = plugins
114
+ state["plugins"].extend(["bump_version"])
115
+
116
+ if should_show_version:
117
+ from importlib.metadata import version
118
+
119
+ print(version("autopub"))
120
+
121
+ raise typer.Exit()
autopub/cli/plugins.py ADDED
@@ -0,0 +1,37 @@
1
+ from importlib import import_module
2
+
3
+ from autopub.plugins import AutopubPlugin
4
+
5
+
6
+ def _find_plugin(module: object) -> type[AutopubPlugin] | None:
7
+ for obj in module.__dict__.values():
8
+ if (
9
+ isinstance(obj, type)
10
+ and issubclass(obj, AutopubPlugin)
11
+ and obj is not AutopubPlugin
12
+ ):
13
+ return obj
14
+
15
+ return None
16
+
17
+
18
+ def find_plugins(names: list[str]) -> list[type[AutopubPlugin]]:
19
+ plugins: list[type] = []
20
+
21
+ for plugin_name in names:
22
+ try:
23
+ # TODO: find plugins outside the autopub namespace
24
+ plugin_module = import_module(f"autopub.plugins.{plugin_name}")
25
+
26
+ plugin_class = _find_plugin(plugin_module)
27
+
28
+ if plugin_class is None:
29
+ print(f"Could not find plugin {plugin_name}")
30
+ # TODO: raise
31
+ continue
32
+
33
+ plugins.append(plugin_class)
34
+ except ImportError as e:
35
+ print(f"Error importing plugin {plugin_name}: {e}")
36
+
37
+ return plugins
autopub/exceptions.py ADDED
@@ -0,0 +1,47 @@
1
+ class AutopubException(Exception):
2
+ message: str
3
+
4
+ def __init__(self) -> None:
5
+ super().__init__(self.message)
6
+
7
+
8
+ class ReleaseFileNotFound(AutopubException):
9
+ message = "Release file not found"
10
+
11
+
12
+ class ReleaseFileEmpty(AutopubException):
13
+ message = "Release file is empty"
14
+
15
+
16
+ class ReleaseNotesEmpty(AutopubException):
17
+ message = "Release notes are empty"
18
+
19
+
20
+ class ReleaseTypeMissing(AutopubException):
21
+ message: str = "Release note is missing release type"
22
+
23
+
24
+ class ReleaseTypeInvalid(AutopubException):
25
+ def __init__(self, release_type: str):
26
+ self.message = f"Release type {release_type} is invalid"
27
+ super().__init__()
28
+
29
+
30
+ class NoPackageManagerPluginFound(AutopubException):
31
+ message = "No package manager plugin found"
32
+
33
+
34
+ class ArtifactNotFound(AutopubException):
35
+ message = "Artifact not found, did you run `autopub check`?"
36
+
37
+
38
+ class ArtifactHashMismatch(AutopubException):
39
+ message = "Artifact hash mismatch, did you run `autopub check`?"
40
+
41
+
42
+ class CommandFailed(AutopubException):
43
+ def __init__(self, command: list[str], returncode: int) -> None:
44
+ self.message = (
45
+ f"Command {' '.join(command)} failed with return code {returncode}"
46
+ )
47
+ super().__init__()
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from typing import Any, Protocol, runtime_checkable
5
+
6
+ from autopub.exceptions import AutopubException, CommandFailed
7
+ from autopub.types import ReleaseInfo
8
+
9
+
10
+ class AutopubPlugin:
11
+ data: dict[str, object] = {}
12
+
13
+ def run_command(self, command: list[str]) -> None:
14
+ try:
15
+ subprocess.run(command, check=True)
16
+ except subprocess.CalledProcessError as e:
17
+ raise CommandFailed(command=command, returncode=e.returncode) from e
18
+
19
+ def prepare(self, release_info: ReleaseInfo) -> None: # pragma: no cover
20
+ ...
21
+
22
+ def validate_release_notes(self, release_info: ReleaseInfo): # pragma: no cover
23
+ ...
24
+
25
+ def on_release_notes_valid(self, release_info: ReleaseInfo): # pragma: no cover
26
+ ...
27
+
28
+ def on_release_notes_invalid(self, exception: AutopubException): # pragma: no cover
29
+ ...
30
+
31
+
32
+ @runtime_checkable
33
+ class AutopubPackageManagerPlugin(Protocol):
34
+ def build(self) -> None: # pragma: no cover
35
+ ...
36
+
37
+ def publish(
38
+ self, repository: str | None = None, **kwargs: Any
39
+ ) -> None: # pragma: no cover
40
+ ...
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+
5
+ import tomlkit
6
+ from dunamai import Version
7
+
8
+ from autopub.plugins import AutopubPlugin
9
+ from autopub.types import ReleaseInfo
10
+
11
+
12
+ class BumpVersionPlugin(AutopubPlugin):
13
+ @property
14
+ def pyproject_config(self) -> tomlkit.TOMLDocument:
15
+ content = pathlib.Path("pyproject.toml").read_text()
16
+
17
+ return tomlkit.parse(content)
18
+
19
+ def _get_version(self, config: tomlkit.TOMLDocument) -> str:
20
+ try:
21
+ return config["tool"]["poetry"]["version"] # type: ignore
22
+ except KeyError:
23
+ return config["project"]["version"] # type: ignore
24
+
25
+ def _update_version(self, config: tomlkit.TOMLDocument, new_version: str) -> None:
26
+ try:
27
+ config["tool"]["poetry"]["version"] = new_version # type: ignore
28
+ except KeyError:
29
+ config["project"]["version"] = new_version # type: ignore
30
+
31
+ def prepare(self, release_info: ReleaseInfo) -> None:
32
+ config = self.pyproject_config
33
+
34
+ version = Version(self._get_version(config))
35
+
36
+ bump_type = {"major": 0, "minor": 1, "patch": 2}[release_info.release_type]
37
+ new_version = version.bump(bump_type).serialize()
38
+
39
+ self._update_version(config, new_version)
40
+
41
+ pathlib.Path("pyproject.toml").write_text(tomlkit.dumps(config)) # type: ignore
autopub/plugins/pdm.py ADDED
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from autopub.plugins import AutopubPackageManagerPlugin, AutopubPlugin
6
+
7
+
8
+ class PDMPlugin(AutopubPlugin, AutopubPackageManagerPlugin):
9
+ def build(self) -> None:
10
+ self.run_command(["pdm", "build"])
11
+
12
+ def publish(self, repository: str | None = None, **kwargs: Any) -> None:
13
+ additional_args: list[str] = []
14
+
15
+ if repository:
16
+ additional_args += ["--repository", repository]
17
+
18
+ self.run_command(["pdm", "publish", *additional_args])
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from autopub.plugins import AutopubPackageManagerPlugin, AutopubPlugin
4
+
5
+
6
+ class PoetryPlugin(AutopubPlugin, AutopubPackageManagerPlugin):
7
+ def build(self) -> None:
8
+ self.run_command(["poetry", "build"])
9
+
10
+ def publish(self, repository: str | None = None, **kwargs: str) -> None:
11
+ additional_args: list[str] = []
12
+
13
+ if repository:
14
+ additional_args += ["--repository", repository]
15
+
16
+ self.run_command(["poetry", "publish", *additional_args])
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date
4
+ from pathlib import Path
5
+
6
+ from autopub.plugins import AutopubPlugin
7
+ from autopub.types import ReleaseInfo
8
+
9
+ # TODO: from config
10
+ CHANGELOG_HEADER = "========="
11
+ VERSION_HEADER = "-"
12
+
13
+
14
+ class UpdateChangelogPlugin(AutopubPlugin):
15
+ @property
16
+ def changelog_file(self) -> Path:
17
+ return Path("CHANGELOG.md")
18
+
19
+ def post_prepare(self, release_info: ReleaseInfo) -> None:
20
+ assert release_info.version is not None
21
+
22
+ if not self.changelog_file.exists():
23
+ self.changelog_file.write_text(f"CHANGELOG\n{CHANGELOG_HEADER}\n\n")
24
+
25
+ current_date = date.today().strftime("%Y-%m-%d")
26
+
27
+ old_changelog_data = ""
28
+ header = ""
29
+
30
+ lines = self.changelog_file.read_text().splitlines()
31
+
32
+ for index, line in enumerate(lines):
33
+ if CHANGELOG_HEADER != line.strip():
34
+ continue
35
+
36
+ old_changelog_data = lines[index + 1 :]
37
+ header = lines[: index + 1]
38
+ break
39
+
40
+ with self.changelog_file.open("w") as f:
41
+ f.write("\n".join(header))
42
+ f.write("\n")
43
+
44
+ new_version_header = f"{release_info.version} - {current_date}"
45
+
46
+ f.write(f"\n{new_version_header}\n")
47
+ f.write(f"{VERSION_HEADER * len(new_version_header)}\n\n")
48
+ f.write(release_info.release_notes)
49
+ f.write("\n")
50
+ f.write("\n".join(old_changelog_data))
autopub/types.py ADDED
@@ -0,0 +1,11 @@
1
+ import dataclasses
2
+
3
+
4
+ @dataclasses.dataclass
5
+ class ReleaseInfo:
6
+ """Release information."""
7
+
8
+ release_type: str
9
+ release_notes: str
10
+ additional_info: dict[str, str] = dataclasses.field(default_factory=dict)
11
+ version: str | None = None
@@ -1,20 +1,19 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: autopub
3
- Version: 0.3.0
3
+ Version: 1.0.0a1
4
4
  Summary: Automatic package release upon pull request merge
5
5
  Home-page: https://github.com/autopub/autopub
6
6
  License: AGPL-3.0
7
7
  Keywords: automatic,packaging,publish,release,version
8
8
  Author: Justin Mayer
9
9
  Author-email: entroP@gmail.com
10
- Requires-Python: >=3.7,<4.0
10
+ Requires-Python: >=3.8,<4.0
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Environment :: Console
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.7
18
17
  Classifier: Programming Language :: Python :: 3.8
19
18
  Classifier: Programming Language :: Python :: 3.9
20
19
  Classifier: Programming Language :: Python :: 3.10
@@ -27,8 +26,12 @@ Requires-Dist: build (>=0.10.0,<0.11.0)
27
26
  Requires-Dist: dunamai (>=1.17.0,<2.0.0)
28
27
  Requires-Dist: githubrelease (>=1.5.9,<2.0.0) ; extra == "github"
29
28
  Requires-Dist: httpx (==0.16.1) ; extra == "github"
29
+ Requires-Dist: python-frontmatter (>=1.0.0,<2.0.0)
30
+ Requires-Dist: rich (>=12.5.1,<13.0.0)
31
+ Requires-Dist: time-machine (>=2.13.0,<3.0.0)
30
32
  Requires-Dist: tomlkit (>=0.5,<2.0)
31
33
  Requires-Dist: twine (>=4.0.2,<5.0.0)
34
+ Requires-Dist: typer (>=0.9.0,<0.10.0)
32
35
  Project-URL: Issue Tracker, https://github.com/autopub/autopub/issues
33
36
  Project-URL: Repository, https://github.com/autopub/autopub
34
37
  Description-Content-Type: text/markdown
@@ -87,3 +90,4 @@ For systems such as Travis CI in which only one deployment step is permitted, th
87
90
  [Travis CI]: https://travis-ci.org
88
91
  [build]: https://pypa-build.readthedocs.io
89
92
  [Twine]: https://twine.readthedocs.io/
93
+
@@ -0,0 +1,15 @@
1
+ autopub/__init__.py,sha256=zcNp8aJE_jo-D5xvDuWNZ-ZK6c67NqbTaKtwfGJWtZs,5576
2
+ autopub/cli/__init__.py,sha256=XG1QVlKPv7svwFOcuosBzXr9AIy9xKDTuOPfI14dtBs,2980
3
+ autopub/cli/plugins.py,sha256=x80BsIpY81o6rv3MJTH2oTAZkqnAYp3MkjUtpPfxGfI,1021
4
+ autopub/exceptions.py,sha256=JYn8sIYWCedhtO3XfftZ0M5M_rAPxiGQ4MGbWUaTYwE,1243
5
+ autopub/plugins/__init__.py,sha256=HtnCcxvZqTUC79vs5aVpNHenRCNuTPpz1gAu2OYkCzg,1153
6
+ autopub/plugins/bump_version.py,sha256=p-_q80pJDCUy-fKhp-9_9HWTWC2pKe2lFJnimuHHH7w,1338
7
+ autopub/plugins/pdm.py,sha256=Pczye06fKg8_HMJDkEfMXQyvao9rZ7sqzTHFd6lLEpU,532
8
+ autopub/plugins/poetry.py,sha256=d2LvW9RI7ZB3reBOXbcp1mqWmzQ06Uyg_T-MxTvlSBg,517
9
+ autopub/plugins/update_changelog.py,sha256=MmDBGFs6v8zPn3NCfUumpF8YODhg5x2AdjyTzgq5bYQ,1456
10
+ autopub/types.py,sha256=FcRH4l27nrkQFUQrbAKxNzPPJfzg_0DExZYsCu9Jdzk,249
11
+ autopub-1.0.0a1.dist-info/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
12
+ autopub-1.0.0a1.dist-info/METADATA,sha256=hJImNEz5eaUoQoG1Ziu44EuCP5Wwjo-s9NBUiFWLiMk,3757
13
+ autopub-1.0.0a1.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
14
+ autopub-1.0.0a1.dist-info/entry_points.txt,sha256=oeTav5NgCxif6mcZ_HeVGgGv5LzS4DwdI01nr4bO1IM,43
15
+ autopub-1.0.0a1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.6.1
2
+ Generator: poetry-core 1.7.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ autopub=autopub.cli:app
3
+
autopub/autopub.py DELETED
@@ -1,83 +0,0 @@
1
- import argparse
2
- import os
3
- import sys
4
-
5
- sys.path.append(os.path.dirname(__file__)) # noqa
6
-
7
- from build_release import build_release
8
- from check_release import check_release
9
- from commit_release import git_commit_and_push
10
- from create_github_release import create_github_release
11
- from deploy_release import deploy_release
12
- from prepare_release import prepare_release
13
- from publish_release import publish_release
14
-
15
-
16
- def check(arguments):
17
- check_release()
18
-
19
-
20
- def prepare(arguments):
21
- prepare_release()
22
-
23
-
24
- def build(arguments):
25
- build_release()
26
-
27
-
28
- def commit(arguments):
29
- git_commit_and_push()
30
-
31
-
32
- def githubrelease(arguments):
33
- create_github_release()
34
-
35
-
36
- def publish(arguments):
37
- publish_release()
38
-
39
-
40
- def deploy(arguments):
41
- deploy_release()
42
-
43
-
44
- def parse_arguments():
45
- try:
46
- version = __import__("pkg_resources").get_distribution("autopub").version
47
- except Exception:
48
- version = "unknown"
49
-
50
- parser = argparse.ArgumentParser()
51
- parser.add_argument("--version", action="version", version=version)
52
-
53
- subparsers = parser.add_subparsers()
54
-
55
- check_parser = subparsers.add_parser("check")
56
- check_parser.set_defaults(func=check)
57
-
58
- prepare_parser = subparsers.add_parser("prepare")
59
- prepare_parser.set_defaults(func=prepare)
60
-
61
- build_parser = subparsers.add_parser("build")
62
- build_parser.set_defaults(func=build)
63
-
64
- commit_parser = subparsers.add_parser("commit")
65
- commit_parser.set_defaults(func=commit)
66
-
67
- githubrelease_parser = subparsers.add_parser("githubrelease")
68
- githubrelease_parser.set_defaults(func=githubrelease)
69
-
70
- publish_parser = subparsers.add_parser("publish")
71
- publish_parser.set_defaults(func=publish)
72
-
73
- deploy_parser = subparsers.add_parser("deploy")
74
- deploy_parser.set_defaults(func=deploy)
75
-
76
- arguments = parser.parse_args()
77
-
78
- return arguments
79
-
80
-
81
- def main():
82
- arguments = parse_arguments()
83
- arguments.func(arguments)
autopub/base.py DELETED
@@ -1,160 +0,0 @@
1
- import os
2
- import re
3
- import subprocess
4
- import sys
5
- from pathlib import Path
6
-
7
- from tomlkit import parse
8
-
9
-
10
- def dict_get(_dict, keys, default=None):
11
- """Query nested dictionary with list of keys, returning None if not found."""
12
- for key in keys:
13
- if isinstance(_dict, dict):
14
- _dict = _dict.get(key, default)
15
- else:
16
- return default
17
- return _dict
18
-
19
-
20
- # Determine CI/CD environment
21
-
22
- if os.environ.get("CIRCLECI"):
23
- CI_SYSTEM = "circleci"
24
- CIRCLE_PROJECT_USERNAME = os.environ.get("CIRCLE_PROJECT_USERNAME")
25
- CIRCLE_PROJECT_REPONAME = os.environ.get("CIRCLE_PROJECT_REPONAME")
26
- REPO_SLUG = f"{CIRCLE_PROJECT_USERNAME}/{CIRCLE_PROJECT_REPONAME}"
27
- elif os.environ.get("GITHUB_ACTIONS") == "true":
28
- CI_SYSTEM = "github"
29
- REPO_SLUG = os.environ.get("GITHUB_REPOSITORY")
30
- elif os.environ.get("TRAVIS"):
31
- CI_SYSTEM = "travis"
32
- REPO_SLUG = os.environ.get("TRAVIS_REPO_SLUG")
33
- else:
34
- CI_SYSTEM = os.environ.get("CI_SYSTEM", None)
35
- REPO_SLUG = os.environ.get("REPO_SLUG", None)
36
-
37
- # Project root and file name configuration
38
-
39
- PROJECT_ROOT = os.environ.get("PROJECT_ROOT")
40
- PYPROJECT_FILE_NAME = os.environ.get("PYPROJECT_FILE_NAME", "pyproject.toml")
41
-
42
- if PROJECT_ROOT:
43
- ROOT = Path(PROJECT_ROOT)
44
- else:
45
- ROOT = Path(
46
- subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
47
- .decode("ascii")
48
- .strip()
49
- )
50
-
51
- PYPROJECT_FILE = ROOT / PYPROJECT_FILE_NAME
52
-
53
- # Retrieve configuration from pyproject file
54
-
55
- if os.path.exists(PYPROJECT_FILE):
56
- config = parse(open(PYPROJECT_FILE).read())
57
- else:
58
- print(f"Could not find pyproject file at: {PYPROJECT_FILE}")
59
- sys.exit(1)
60
-
61
- PROJECT_NAME = dict_get(config, ["tool", "autopub", "project-name"])
62
- if not PROJECT_NAME:
63
- PROJECT_NAME = dict_get(config, ["tool", "poetry", "name"])
64
- if not PROJECT_NAME:
65
- PROJECT_NAME = dict_get(config, ["project", "name"])
66
- if not PROJECT_NAME:
67
- print(
68
- "Could not determine project name. Under the pyproject file's "
69
- '[tool.autopub] header, add:\nproject-name = "YourProjectName"'
70
- )
71
- sys.exit(1)
72
-
73
- RELEASE_FILE_NAME = dict_get(
74
- config, ["tool", "autopub", "release-file"], default="RELEASE.md"
75
- )
76
- RELEASE_FILE = ROOT / RELEASE_FILE_NAME
77
-
78
- CHANGELOG_FILE_NAME = dict_get(
79
- config, ["tool", "autopub", "changelog-file"], default="CHANGELOG.md"
80
- )
81
- CHANGELOG_FILE = ROOT / CHANGELOG_FILE_NAME
82
-
83
- CHANGELOG_HEADER = dict_get(
84
- config, ["tool", "autopub", "changelog-header"], default="========="
85
- )
86
-
87
- VERSION_HEADER = dict_get(config, ["tool", "autopub", "version-header"], default="-")
88
- VERSION_STRINGS = dict_get(config, ["tool", "autopub", "version-strings"], default=[])
89
-
90
- TAG_PREFIX = dict_get(config, ["tool", "autopub", "tag-prefix"], default="")
91
-
92
- PYPI_URL = dict_get(config, ["tool", "autopub", "pypi-url"])
93
-
94
- # Git configuration
95
-
96
- GIT_USERNAME = dict_get(config, ["tool", "autopub", "git-username"])
97
- GIT_EMAIL = dict_get(config, ["tool", "autopub", "git-email"])
98
-
99
- # GitHub
100
-
101
- GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", None)
102
- APPEND_GITHUB_CONTRIBUTOR = dict_get(
103
- config, ["tool", "autopub", "append-github-contributor"], False
104
- )
105
-
106
-
107
- def run_process(popenargs, encoding="utf-8", env=None):
108
- if env is not None:
109
- env = {**os.environ, **env}
110
- try:
111
- return subprocess.check_output(popenargs, encoding=encoding, env=env).strip()
112
- except subprocess.CalledProcessError as e:
113
- print(e.output, file=sys.stderr)
114
- sys.exit(1)
115
-
116
-
117
- def git(popenargs):
118
- # Do not decode ASCII for commit messages so emoji are preserved
119
- return subprocess.check_output(["git", *popenargs])
120
-
121
-
122
- def check_exit_code(popenargs):
123
- return subprocess.call(popenargs, shell=True)
124
-
125
-
126
- def get_project_version():
127
- # Backwards compat: Try poetry first and then fall "back" to standards
128
- version = dict_get(config, ["tool", "poetry", "version"])
129
- if version is None:
130
- return dict_get(config, ["project", "version"])
131
- else:
132
- return version
133
-
134
-
135
- def get_release_info():
136
- RELEASE_TYPE_REGEX = re.compile(r"^[Rr]elease [Tt]ype: (major|minor|patch)$")
137
-
138
- with open(RELEASE_FILE, "r") as f:
139
- line = f.readline()
140
- match = RELEASE_TYPE_REGEX.match(line)
141
-
142
- if not match:
143
- print(
144
- "The RELEASE file should start with 'Release type' and "
145
- "specify one of the following values: major, minor, or patch."
146
- )
147
- sys.exit(1)
148
-
149
- type_ = match.group(1)
150
- changelog = "".join([l for l in f.readlines()]).strip()
151
-
152
- return type_, changelog
153
-
154
-
155
- def configure_git():
156
- if not GIT_USERNAME or not GIT_EMAIL:
157
- print("git-username and git-email must be defined in the pyproject file")
158
- sys.exit(1)
159
- git(["config", "user.name", GIT_USERNAME])
160
- git(["config", "user.email", GIT_EMAIL])
autopub/build_release.py DELETED
@@ -1,14 +0,0 @@
1
- import os
2
- import sys
3
-
4
- sys.path.append(os.path.dirname(__file__)) # noqa
5
-
6
- from base import git, run_process
7
-
8
-
9
- def build_release():
10
- env = None
11
- if "SOURCE_DATE_EPOCH" not in os.environ:
12
- ctime = git(["log", "-1", "--pretty=%ct"]).decode().strip()
13
- env = {"SOURCE_DATE_EPOCH": ctime}
14
- run_process([sys.executable, "-m", "build"], env=env)
autopub/check_release.py DELETED
@@ -1,19 +0,0 @@
1
- import os
2
- import sys
3
-
4
- sys.path.append(os.path.dirname(__file__)) # noqa
5
-
6
- from base import CI_SYSTEM, RELEASE_FILE, run_process
7
-
8
-
9
- def check_release():
10
- needs_release = os.path.exists(RELEASE_FILE)
11
- if not needs_release:
12
- print("Not releasing a new version because there is no RELEASE file.")
13
- if CI_SYSTEM == "circleci":
14
- run_process(["circleci", "step", "halt"])
15
- elif CI_SYSTEM == "travis":
16
- sys.exit(1)
17
- if CI_SYSTEM == "github":
18
- with open(os.path.expandvars("$GITHUB_OUTPUT"), "a") as f:
19
- f.write("autopub_release={}\n".format("true" if needs_release else "false"))
autopub/commit_release.py DELETED
@@ -1,33 +0,0 @@
1
- import os
2
- import sys
3
-
4
- sys.path.append(os.path.dirname(__file__)) # noqa
5
-
6
- from base import (
7
- get_project_version,
8
- git,
9
- configure_git,
10
- PROJECT_NAME,
11
- PYPROJECT_FILE_NAME,
12
- CHANGELOG_FILE_NAME,
13
- RELEASE_FILE_NAME,
14
- VERSION_STRINGS,
15
- )
16
-
17
-
18
- def git_commit_and_push():
19
- configure_git()
20
-
21
- version = get_project_version()
22
-
23
- git(["add", PYPROJECT_FILE_NAME])
24
- git(["add", CHANGELOG_FILE_NAME])
25
-
26
- if VERSION_STRINGS:
27
- for version_file in VERSION_STRINGS:
28
- git(["add", version_file])
29
-
30
- git(["rm", "--cached", RELEASE_FILE_NAME])
31
-
32
- git(["commit", "-m", f"Release {PROJECT_NAME} {version}"])
33
- git(["push", "origin", "HEAD"])
@@ -1,66 +0,0 @@
1
- import os
2
- import sys
3
- import time
4
-
5
- sys.path.append(os.path.dirname(__file__)) # noqa
6
-
7
- from base import (
8
- run_process,
9
- check_exit_code,
10
- get_project_version,
11
- configure_git,
12
- PROJECT_NAME,
13
- REPO_SLUG,
14
- TAG_PREFIX,
15
- get_release_info,
16
- )
17
-
18
-
19
- def create_github_release():
20
- try:
21
- from github_release import gh_release_create, gh_asset_upload
22
- except ModuleNotFoundError:
23
- print("Cannot create GitHub release due to missing dependency: github_release")
24
- sys.exit(1)
25
-
26
- configure_git()
27
- version = get_project_version()
28
- tag = f"{TAG_PREFIX}{version}"
29
-
30
- if not version:
31
- print("Unable to determine the current version")
32
- sys.exit(1)
33
-
34
- tag_exists = (
35
- check_exit_code([f'git show-ref --tags --quiet --verify -- "refs/tags/{tag}"'])
36
- == 0
37
- )
38
-
39
- if not tag_exists:
40
- run_process(["git", "tag", tag])
41
- run_process(["git", "push", "--tags"])
42
-
43
- _, changelog = get_release_info()
44
-
45
- gh_release_create(
46
- REPO_SLUG,
47
- tag,
48
- publish=True,
49
- name=f"{PROJECT_NAME} {version}",
50
- body=changelog,
51
- )
52
-
53
- # give some time to the API to get updated
54
- # not sure if this is the proper way to fix the issue
55
- # ideally the githubrelease package shouldn't need
56
- # to do another API call to get the release since it
57
- # should be returned by the create API.
58
- # anyway, this fix might be good enough for the time being
59
-
60
- time.sleep(2)
61
-
62
- gh_asset_upload(
63
- REPO_SLUG,
64
- tag,
65
- pattern="dist/*",
66
- )
autopub/deploy_release.py DELETED
@@ -1,18 +0,0 @@
1
- import os
2
- import sys
3
-
4
- sys.path.append(os.path.dirname(__file__)) # noqa
5
-
6
- from build_release import build_release
7
- from create_github_release import create_github_release
8
- from commit_release import git_commit_and_push
9
- from prepare_release import prepare_release
10
- from publish_release import publish_release
11
-
12
-
13
- def deploy_release():
14
- prepare_release()
15
- build_release()
16
- git_commit_and_push()
17
- create_github_release()
18
- publish_release()
@@ -1,83 +0,0 @@
1
- import os
2
- import subprocess
3
- import sys
4
-
5
- sys.path.append(os.path.dirname(__file__)) # noqa
6
-
7
- from base import GITHUB_TOKEN, REPO_SLUG, APPEND_GITHUB_CONTRIBUTOR
8
-
9
-
10
- def append_github_contributor(file):
11
- if not APPEND_GITHUB_CONTRIBUTOR:
12
- return
13
-
14
- try:
15
- import httpx
16
- except ModuleNotFoundError:
17
- print("Cannot append the GitHub contributor due to missing dependency: httpx")
18
- sys.exit(1)
19
-
20
- org, repo = REPO_SLUG.split("/")
21
- current_commit = (
22
- subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("ascii").strip()
23
- )
24
-
25
- response = httpx.post(
26
- "https://api.github.com/graphql",
27
- json={
28
- "query": """query Contributor(
29
- $owner: String!
30
- $name: String!
31
- $commit: GitObjectID!
32
- ) {
33
- repository(owner: $owner, name: $name) {
34
- object(oid: $commit) {
35
- __typename
36
- ... on Commit {
37
- associatedPullRequests(first: 1) {
38
- nodes {
39
- number
40
- author {
41
- __typename
42
- login
43
- ... on User {
44
- name
45
- }
46
- }
47
- }
48
- }
49
- }
50
- }
51
- }
52
- }""",
53
- "variables": {"owner": org, "name": repo, "commit": current_commit},
54
- },
55
- headers={
56
- "Content-Type": "application/json",
57
- "Authorization": f"Bearer {GITHUB_TOKEN}",
58
- },
59
- )
60
-
61
- payload = response.json()
62
- commit = payload["data"]["repository"]["object"]
63
-
64
- if not commit:
65
- return
66
-
67
- prs = commit["associatedPullRequests"]["nodes"]
68
-
69
- if not prs:
70
- return
71
-
72
- pr = prs[0]
73
-
74
- pr_number = pr["number"]
75
- pr_author_username = pr["author"]["login"]
76
- pr_author_fullname = pr["author"].get("name", "")
77
-
78
- file.write("\n")
79
- file.write("\n")
80
- file.write(
81
- f"Contributed by [{pr_author_fullname or pr_author_username}](https://github.com/{pr_author_username}) via [PR #{pr_number}](https://github.com/{REPO_SLUG}/pull/{pr_number}/)"
82
- )
83
- file.write("\n")
@@ -1,98 +0,0 @@
1
- import os
2
- import re
3
- import sys
4
-
5
- sys.path.append(os.path.dirname(__file__)) # noqa
6
-
7
- from datetime import datetime
8
-
9
- import tomlkit
10
- from base import (
11
- CHANGELOG_FILE,
12
- CHANGELOG_HEADER,
13
- PYPROJECT_FILE,
14
- ROOT,
15
- VERSION_HEADER,
16
- VERSION_STRINGS,
17
- configure_git,
18
- dict_get,
19
- get_project_version,
20
- get_release_info,
21
- )
22
- from dunamai import Version
23
- from github_contributor import append_github_contributor
24
-
25
-
26
- def update_version_strings(file_path, new_version):
27
- version_regex = re.compile(r"(^_*?version_*?\s*=\s*['\"])(\d+\.\d+\.\d+)", re.M)
28
- with open(file_path, "r+") as f:
29
- content = f.read()
30
- f.seek(0)
31
- f.write(
32
- re.sub(
33
- version_regex,
34
- lambda match: "{}{}".format(match.group(1), new_version),
35
- content,
36
- )
37
- )
38
- f.truncate()
39
-
40
-
41
- def prepare_release():
42
- configure_git()
43
-
44
- type_, release_changelog = get_release_info()
45
-
46
- version = Version(get_project_version())
47
- new_version = version.bump({"major": 0, "minor": 1, "patch": 2}[type_]).serialize()
48
-
49
- with open(PYPROJECT_FILE, "r") as f:
50
- config = tomlkit.load(f)
51
-
52
- poetry = dict_get(config, ["tool", "poetry", "version"])
53
- if poetry:
54
- config["tool"]["poetry"]["version"] = new_version
55
- else:
56
- config["project"]["version"] = new_version
57
-
58
- with open(PYPROJECT_FILE, "w") as f:
59
- config = tomlkit.dump(config, f)
60
-
61
- if VERSION_STRINGS:
62
- for version_file in VERSION_STRINGS:
63
- file_path = ROOT / version_file
64
- update_version_strings(file_path, new_version)
65
-
66
- current_date = datetime.utcnow().strftime("%Y-%m-%d")
67
-
68
- old_changelog_data = ""
69
- header = ""
70
-
71
- if not CHANGELOG_FILE.is_file():
72
- with open(CHANGELOG_FILE, "a+") as f:
73
- f.write(f"CHANGELOG\n{CHANGELOG_HEADER}\n\n")
74
-
75
- with open(CHANGELOG_FILE, "r") as f:
76
- lines = f.readlines()
77
-
78
- for index, line in enumerate(lines):
79
- if CHANGELOG_HEADER != line.strip():
80
- continue
81
-
82
- old_changelog_data = lines[index + 1 :]
83
- header = lines[: index + 1]
84
- break
85
-
86
- with open(CHANGELOG_FILE, "w") as f:
87
- f.write("".join(header))
88
-
89
- new_version_header = f"{new_version} - {current_date}"
90
-
91
- f.write(f"\n{new_version_header}\n")
92
- f.write(f"{VERSION_HEADER * len(new_version_header)}\n\n")
93
-
94
- f.write(release_changelog)
95
- append_github_contributor(f)
96
- f.write("\n")
97
-
98
- f.write("".join(old_changelog_data))
@@ -1,17 +0,0 @@
1
- import glob
2
- import os
3
- import sys
4
-
5
- sys.path.append(os.path.dirname(__file__)) # noqa
6
-
7
- from base import PYPI_URL, run_process
8
-
9
-
10
- def publish_release():
11
- env = None
12
- if PYPI_URL:
13
- env = {"TWINE_REPOSITORY_URL": PYPI_URL}
14
- dists = glob.glob("dist/*")
15
- run_process(
16
- [sys.executable, "-m", "twine", "upload", "--non-interactive", *dists], env=env
17
- )
@@ -1,16 +0,0 @@
1
- autopub/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- autopub/autopub.py,sha256=uY-CGv6NIamoZYdv3U5oA4GDJZ-MBSmn2v9X5YRjn6w,1908
3
- autopub/base.py,sha256=DUldASRwL8F3nWY5jXnzW0bY_4RIClvOVIK94EBxJyw,4794
4
- autopub/build_release.py,sha256=UM9AbDHJWn4FpkeTJfm6PcAOMu1ma_xArTKk9202H4w,361
5
- autopub/check_release.py,sha256=XcoX067xrJ1B1A7z0Y50Lj9zt4ELLEKjWRDf318y9fg,641
6
- autopub/commit_release.py,sha256=Tz-TToqEO9AaWz5HUCNrQV3mlusdyI3HaxmwvHsPIT4,680
7
- autopub/create_github_release.py,sha256=6jjp-7cU4ERZjNVpp8tR7U6k-hUmB7VCzC9JiBfKu70,1558
8
- autopub/deploy_release.py,sha256=J0hkMhLaO-ktKMjNa-Zu1pVngx_fD_nM_dzC3UTWJM4,447
9
- autopub/github_contributor.py,sha256=z49DxFGuvjMimUbKBP9BtsYmNLGuEW22tsPZkL_qL-0,2466
10
- autopub/prepare_release.py,sha256=Ku4ZDXeTRYpNwqAY02GYzTB2umsPlZUFs5DZUzA6jho,2537
11
- autopub/publish_release.py,sha256=xxHI2SiXIFrJPRUP5EVUWrsozJIW8HiIgSLDgzt6s2U,374
12
- autopub-0.3.0.dist-info/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
13
- autopub-0.3.0.dist-info/METADATA,sha256=dWx1JeRanG58-PZTxBGRqH7Mrvr_QeNHIuVd_TzL9UA,3629
14
- autopub-0.3.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
15
- autopub-0.3.0.dist-info/entry_points.txt,sha256=dneC-sBNh-ntAab9sb46B2D6zW7GGvXHHBtIGyknjnA,48
16
- autopub-0.3.0.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- autopub=autopub.autopub:main
3
-