haraka 0.2.5__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.
haraka/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """cookiecutter post-generation helper package."""
2
+ from haraka.post_gen.runner import main
3
+ from haraka.post_gen.config import PostGenConfig
4
+ __all__ = ["main", "PostGenConfig"]
haraka/art/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .ascii.frame import framer
2
+ from haraka.art.ascii.frame import TextFramer
3
+ __all__ = ["TextFramer", "framer"]
File without changes
@@ -0,0 +1,200 @@
1
+ goLang = f"""
2
+ \u200B ____ _ \u200B
3
+ \u200B / ___| ___ | | __ _ _ __ __ _ \u200B
4
+ \u200B | | _ / _ \| | / _` | '_ \ / _` | \u200B
5
+ \u200B | |_| | (_) | |__| (_| | | | | (_| | \u200B
6
+ \u200B \____|\___/|_____\__,_|_| |_|\__, | \u200B
7
+ \u200B |___/ \u200B
8
+ """
9
+ protoC_Mode = """
10
+ \u200B ____ _ ____ __ __ _ \u200B
11
+ \u200B | _ \ _ __ ___ | |_ ___ / ___| | \/ | ___ __| | ___ \u200B
12
+ \u200B | |_) | '__/ _ \| __/ _ \| | | |\/| |/ _ \ / _` |/ _ \ \u200B
13
+ \u200B | __/| | | (_) | || (_) | |___ | | | | (_) | (_| | __/ \u200B
14
+ \u200B |_| |_| \___/ \__\___/ \____| |_| |_|\___/ \__,_|\___| \u200B
15
+ """
16
+ goFast = f"""
17
+ \u200B ____ ___ _____ _ ____ _____ \u200B
18
+ \u200B / ___|/ _ \ | ___|/ \ / ___| _ _| \u200B
19
+ \u200B | | _| | | | | |__ / _ \ \___ \ | | \u200B
20
+ \u200B | |_| | |_| | | |__/ ___ \ ___) | | | \u200B
21
+ \u200B \___/ \___ / |_| /_/ \_\____/ |_| \u200B
22
+ """
23
+
24
+ emoji = {
25
+ "goFast": f"""
26
+ \u200B 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 \u200B
27
+ \u200B 🟩🟩⬜⬜⬜⬜🟩🟩🟩⬜⬜⬜🟩🟩🟩⬜⬜⬜⬜⬜🟩🟩🟩🟩⬜🟩🟩🟩🟩🟩⬜⬜⬜⬜🟩🟩⬜⬜⬜⬜⬜🟩 \u200B
28
+ \u200B 🟩⬜🟩🟩🟩🟩🟩🟩⬜🟩🟩🟩⬜🟩🟩⬜🟩🟩🟩🟩🟩🟩🟩⬜🟩⬜🟩🟩🟩⬜🟩🟩🟩🟩🟩🟩🟩🟩⬜🟩🟩🟩 \u200B
29
+ \u200B 🟩⬜🟩🟩⬜⬜🟩🟩⬜🟩🟩🟩⬜🟩🟩⬜⬜⬜⬜🟩🟩🟩⬜🟩🟩🟩⬜🟩🟩🟩⬜⬜⬜🟩🟩🟩🟩🟩⬜🟩🟩🟩 \u200B
30
+ \u200B 🟩⬜🟩🟩🟩⬜🟩🟩⬜🟩🟩🟩⬜🟩🟩⬜🟩🟩🟩🟩🟩🟩⬜⬜⬜⬜⬜🟩🟩🟩🟩🟩🟩⬜🟩🟩🟩🟩⬜🟩🟩🟩 \u200B
31
+ \u200B 🟩🟩⬜⬜⬜⬜🟩🟩🟩⬜⬜⬜🟩🟩🟩⬜🟩🟩🟩🟩🟩🟩⬜🟩🟩🟩⬜🟩🟩⬜⬜⬜⬜🟩🟩🟩🟩🟩⬜🟩🟩🟩 \u200B
32
+ \u200B 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 \u200B
33
+ """,
34
+ "go": f"""
35
+ \u200B🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩\u200B
36
+ \u200B🟩🟩⬜⬜⬜⬜🟩🟩🟩⬜⬜⬜🟩🟩\u200B
37
+ \u200B🟩⬜🟩🟩🟩🟩🟩🟩⬜🟩🟩🟩⬜🟩\u200B
38
+ \u200B🟩⬜🟩🟩⬜⬜🟩🟩⬜🟩🟩🟩⬜🟩\u200B
39
+ \u200B🟩⬜🟩🟩🟩⬜🟩🟩⬜🟩🟩🟩⬜🟩\u200B
40
+ \u200B🟩🟩⬜⬜⬜⬜🟩🟩🟩⬜⬜⬜🟩🟩\u200B
41
+ \u200B🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩\u200B
42
+ """
43
+ }
44
+
45
+ gRpc_ProtoBuf = f"""
46
+ \u200B ____ ____ ____ ____ _ ____ __ \u200B
47
+ \u200B __ _| _ \| _ \ / ___| | _ \ _ __ ___ | |_ ___ | __ ) _ _ / _| \u200B
48
+ \u200B / _` | |_) | |_) | | _____| |_) | '__/ _ \| __/ _ \| _ \| | | | |_| \u200B
49
+ \u200B | (_| | _ <| __/| |__|_____| __/| | | (_) | || (_) | |_) | |_| | _| \u200B
50
+ \u200B \__, |_| \_\_| \____| |_| |_| \___/ \__\___/|____/ \__,_|_| \u200B
51
+ \u200B |___/ \u200B
52
+ """
53
+ swaggerUI = """
54
+ \u200B ____ _ _ ___ \u200B
55
+ \u200B / ___|_ ____ _ __ _ __ _ ___ _ __| | | |_ _| \u200B
56
+ \u200B \___ \ \ /\ / / _` |/ _` |/ _` |/ _ \ '__| | | || | \u200B
57
+ \u200B ___) \ V V / (_| | (_| | (_| | __/ | | |_| || | \u200B
58
+ \u200B |____/ \_/\_/ \__,_|\__, |\__, |\___|_| \___/|___| \u200B
59
+ \u200B |___/ |___/ \u200B
60
+ """
61
+ server = """
62
+ \u200B ____ \u200B
63
+ \u200B / ___| ___ _ ____ _____ _ __ \u200B
64
+ \u200B \___ \ / _ \ '__\ \ / / _ \ '__| \u200B
65
+ \u200B ___) | __/ | \ V / __/ | \u200B
66
+ \u200B |____/ \___|_| \_/ \___|_| \u200B
67
+ """
68
+ divider_mono = f"""
69
+ \u200B _____ \u200B
70
+ \u200B |_____| \u200B
71
+ """
72
+ divider_xs = f"""
73
+ \u200B _____ _____ \u200B
74
+ \u200B |_____|_____| \u200B
75
+ """
76
+ divider_s = f"""
77
+ \u200B _____ _____ _____ \u200B
78
+ \u200B |_____|_____|_____| \u200B
79
+ """
80
+ divider_m = f"""
81
+ \u200B _____ _____ _____ _____ _____ \u200B
82
+ \u200B |_____|_____|_____|_____|_____| \u200B
83
+ """
84
+ divider_l = f"""
85
+ \u200B _____ _____ _____ _____ _____ _____ _____ _____ \u200B
86
+ \u200B |_____|_____|_____|_____|_____|_____|_____|_____| \u200B
87
+ """
88
+ divider_xl = f"""
89
+ \u200B _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ _____ \u200B
90
+ \u200B |_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____| \u200B
91
+ """
92
+ by_google = f"""
93
+ \u200B _ ____ _ \u200B
94
+ \u200B | |__ _ _ / ___| ___ ___ __ _| | ___ \u200B
95
+ \u200B | '_ \| | | | | | _ / _ \ / _ \ / _` | |/ _ \ \u200B
96
+ \u200B | |_) | |_| | | |_| | (_) | (_) | (_| | | __/ \u200B
97
+ \u200B |_.__/ \__, | \____|\___/ \___/ \__, |_|\___| \u200B
98
+ \u200B |___/ |___/ \u200B
99
+ """
100
+ by = """
101
+ \u200B _____ _ __ \u200B
102
+ \u200B | __ ) \ / / \u200B
103
+ \u200B | _ \ \ V / \u200B
104
+ \u200B | |_) | | | \u200B
105
+ \u200B |____/ |_| \u200B
106
+ """
107
+ wjb_dev = f"""
108
+ \u200B __ __ _ ____ ____ _______ __ \u200B
109
+ \u200B \ \ / / | | __ ) | _ \| ____\ \ / / \u200B
110
+ \u200B \ \ /\ / / | | _ \ | | | | _| \ \ / / \u200B
111
+ \u200B \ V V / |_| | |_) || |_| | |___ \ V / \u200B
112
+ \u200B \_/\_/ \___/|____(_)____/|_____| \_/ \u200B
113
+ """
114
+
115
+ gRPCurl = f"""
116
+ \u200B ____ ____ ____ _ \u200B
117
+ \u200B __ _| _ \| _ \ / ___| _ _ __| | \u200B
118
+ \u200B / _` | |_) | |_) | | | | | | '__| | \u200B
119
+ \u200B | (_| | _ <| __/| |__| |_| | | | | \u200B
120
+ \u200B \__, |_| \_\_| \____\__,_|_| |_| \u200B
121
+ \u200B |___/ \u200B
122
+ """
123
+ zeroLog = """
124
+ \u200B _____ _ \u200B
125
+ \u200B |__ /___ _ __ ___ | | ___ __ _ \u200B
126
+ \u200B / // _ \ '__/ _ \| | / _ \ / _` | \u200B
127
+ \u200B / /| __/ | | (_) | |__| (_) | (_| | \u200B
128
+ \u200B /____\___|_| \___/|_____\___/ \__, | \u200B
129
+ \u200B |___/ \u200B
130
+ """
131
+ gRPC = f"""
132
+ \u200B ____ ____ ____ \u200B
133
+ \u200B __ _| _ \| _ \ / ___| \u200B
134
+ \u200B / _` | |_) | |_) | | \u200B
135
+ \u200B | (_| | _ <| __/| |___ \u200B
136
+ \u200B \__, |_| \_\_| \____| \u200B
137
+ \u200B |___/ \u200B
138
+ """
139
+ autoMaxProcs = f"""
140
+ \u200B _ _ __ __ ____ \u200B
141
+ \u200B / \ _ _| |_ ___ | \/ | __ ___ _| _ \ _ __ ___ ___ ___ \u200B
142
+ \u200B / _ \| | | | __/ _ \| |\/| |/ _` \ \/ / |_) | '__/ _ \ / __/ __| \u200B
143
+ \u200B / ___ \ |_| | || (_) | | | | (_| |> <| __/| | | (_) | (__\__ \ \u200B
144
+ \u200B /_/ \_\__,_|\__\___/|_| |_|\__,_/_/\_\_| |_| \___/ \___|___/ \u200B
145
+ """
146
+ ants = f"""
147
+ \u200B _ _ \u200B
148
+ \u200B / \ _ __ | |_ ___ \u200B
149
+ \u200B / _ \ | '_ \| __/ __| \u200B
150
+ \u200B / ___ \| | | | |_\__ \ \u200B
151
+ \u200B /_/ \_\_| |_|\__|___/ \u200B
152
+ """
153
+ protoC = f"""
154
+ \u200B ____ _ ____ \u200B
155
+ \u200B | _ \ _ __ ___ | |_ ___ / ___| \u200B
156
+ \u200B | |_) | '__/ _ \| __/ _ \| | \u200B
157
+ \u200B | __/| | | (_) | || (_) | |___ \u200B
158
+ \u200B |_| |_| \___/ \__\___/ \____| \u200B
159
+ """
160
+ performance_mode = """
161
+ \u200B ____ __ __ __ _ \u200B
162
+ \u200B | _ \ ___ _ __ / _| ___ _ __ _ __ ___ __ _ _ __ ___ ___ | \/ | ___ __| | ___ \u200B
163
+ \u200B | |_) / _ \ '__| |_ / _ \| '__| '_ ` _ \ / _` | '_ \ / __/ _ \ | |\/| |/ _ \ / _` |/ _ \ \u200B
164
+ \u200B | __/ __/ | | _| (_) | | | | | | | | (_| | | | | (_| __/ | | | | (_) | (_| | __/ \u200B
165
+ \u200B |_| \___|_| |_| \___/|_| |_| |_| |_|\__,_|_| |_|\___\___| |_| |_|\___/ \__,_|\___| \u200B
166
+ """
167
+ tools = f"""
168
+ \u200B _____ _ \u200B
169
+ \u200B |_ _|__ ___ | |___ \u200B
170
+ \u200B | |/ _ \ / _ \| / __| \u200B
171
+ \u200B | | (_) | (_) | \__ \ \u200B
172
+ \u200B |_|\___/ \___/|_|___/ \u200B
173
+ """
174
+
175
+ __all__ = [
176
+ "goLang",
177
+ "protoC_Mode",
178
+ "goFast",
179
+ "gRpc_ProtoBuf",
180
+ "swaggerUI",
181
+ "server",
182
+ "divider_mono",
183
+ "divider_xs",
184
+ "divider_s",
185
+ "divider_m",
186
+ "divider_l",
187
+ "divider_xl",
188
+ "by_google",
189
+ "by",
190
+ "wjb_dev",
191
+ "emoji",
192
+ "gRPCurl",
193
+ "zeroLog",
194
+ "gRPC",
195
+ "autoMaxProcs",
196
+ "ants",
197
+ "protoC",
198
+ "performance_mode",
199
+ "tools",
200
+ ]
@@ -0,0 +1,5 @@
1
+ from .width_utils import WidthUtil
2
+ from .border import BorderBuilder
3
+ from .framer import TextFramer
4
+
5
+ __all__ = ["WidthUtil", "BorderBuilder", "TextFramer"]
@@ -0,0 +1,44 @@
1
+ """
2
+ Everything about the **horizontal** border (top / bottom).
3
+ """
4
+ import shutil
5
+
6
+
7
+ class BorderBuilder:
8
+ """
9
+ Builds a single horizontal line – optionally centred – that is
10
+ `pattern`-wide and `fraction` of the terminal width.
11
+ """
12
+
13
+ def __init__(self, pattern: str = "=", fraction: float = 0.80, center: bool = True):
14
+ self.pattern = pattern
15
+ self.fraction = fraction
16
+ self.center = center
17
+
18
+ # ------- public API ------------------------------------------------ #
19
+
20
+ def build(self) -> str:
21
+ """Return the fully formed border line."""
22
+ horiz = self._repeat_pattern(self.width)
23
+
24
+ if not self.center:
25
+ return horiz
26
+
27
+ margin = (self.term_width - self.width) // 2
28
+ return " " * margin + horiz if margin > 0 else horiz
29
+
30
+ # ------- internals -------------------------------------------------- #
31
+
32
+ @property
33
+ def term_width(self) -> int:
34
+ return max(1, shutil.get_terminal_size(fallback=(80, 24)).columns)
35
+
36
+ @property
37
+ def width(self) -> int:
38
+ return max(1, int(self.term_width * self.fraction))
39
+
40
+ def _repeat_pattern(self, target: int) -> str:
41
+ if not self.pattern:
42
+ return " " * target
43
+ reps = (target // len(self.pattern)) + 1
44
+ return (self.pattern * reps)[:target]
@@ -0,0 +1,125 @@
1
+ """
2
+ High-level Β«drop-inΒ» that turns a list[str] into a fully framed banner.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import shutil
7
+ import textwrap
8
+ from typing import List
9
+
10
+ from .border import BorderBuilder
11
+
12
+
13
+ class TextFramer:
14
+ """
15
+ Build a rectangular frame:
16
+
17
+ β€’ configurable borders (x / y chars)
18
+ β€’ configurable padding and alignment
19
+ β€’ automatically wraps lines to fit inside
20
+
21
+ Example
22
+ -------
23
+ >>> framer = TextFramer(border_char_x="=", border_char_y="|")
24
+ >>> print(framer.frame(["Hello", "world"]))
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ *,
30
+ border_char_x: str = "=",
31
+ border_char_y: str = "|",
32
+ padding: int = 1,
33
+ align: str = "center",
34
+ border_fraction: float = 0.80,
35
+ center_border: bool = True,
36
+ flex: bool = False,
37
+ art: str = ""
38
+ ):
39
+ self.border_x = border_char_x
40
+ self.border_y = border_char_y
41
+ self.padding = max(0, padding)
42
+ self.align = align.lower()
43
+ self.border_fraction = border_fraction
44
+ self.center_border = center_border
45
+ self.flex = flex
46
+
47
+ # ---- public faΓ§ade ----------------------------------------------- #
48
+
49
+ def frame(self, texts: List[str]) -> str:
50
+ """
51
+ Return one string with \n-separated lines ready for printing.
52
+ """
53
+ lines = self._normalise(texts)
54
+ if not lines:
55
+ return ""
56
+
57
+ # 1️⃣ build top/bottom borders once
58
+ bb = BorderBuilder(self.border_x, self.border_fraction, self.center_border)
59
+ top_border = bb.build()
60
+ bottom_border = top_border # symmetrical
61
+ target_width = bb.width
62
+ if self.flex:
63
+ margin = (self.term_width - target_width) // 2 if self.center_border else 0
64
+ else :
65
+ margin = (110 - target_width) // 2 if self.center_border else 0
66
+
67
+ # 2️⃣ compute interior geometry
68
+ side = self.border_y or ""
69
+ side_w = len(side)
70
+ interior_w = max(1, target_width - 2 * side_w - 2 * self.padding)
71
+
72
+ # 3️⃣ iterate through wrapped chunks
73
+ frame: list[str] = [top_border]
74
+ for logical in lines:
75
+ chunks = textwrap.wrap(logical, width=interior_w) or [""]
76
+ for chunk in chunks:
77
+ frame.append(self._compose_line(chunk, interior_w, side, side_w, margin, target_width))
78
+
79
+ frame.append(bottom_border)
80
+ return "\n".join(frame)
81
+
82
+ # ---- helpers ------------------------------------------------------ #
83
+
84
+ @property
85
+ def term_width(self) -> int:
86
+ return max(1, shutil.get_terminal_size(fallback=(80, 24)).columns)
87
+
88
+ @staticmethod
89
+ def _normalise(texts: List[str]):
90
+ text = " ".join(texts)
91
+ return textwrap.dedent(text).strip("\n").splitlines()
92
+
93
+ # Build one interior line ------------------------------------------ #
94
+ def _compose_line(
95
+ self,
96
+ chunk: str,
97
+ interior_w: int,
98
+ side: str,
99
+ side_w: int,
100
+ margin: int,
101
+ target_w: int,
102
+ ) -> str:
103
+ extra = interior_w - len(chunk)
104
+ if self.align == "left":
105
+ left_x, right_x = 0, extra
106
+ elif self.align == "right":
107
+ left_x, right_x = extra, 0
108
+ else: # centre
109
+ left_x, right_x = divmod(extra, 2)
110
+
111
+ left_pad = " " * (self.padding + left_x)
112
+ right_pad = " " * (self.padding + right_x)
113
+ body = f"{side}{left_pad}{chunk}{right_pad}{side}"
114
+
115
+ # pad / trim to exact width
116
+ if len(body) < target_w:
117
+ body = body[:-side_w] + " " * (target_w - len(body)) + body[-side_w:]
118
+ elif len(body) > target_w:
119
+ interior_allowed = target_w - 2 * side_w
120
+ body = f"{side}{body[side_w : side_w + interior_allowed]}{side}"
121
+
122
+ return " " * margin + body if margin else body
123
+
124
+ def generate(self, texts: List[str]):
125
+ return print(self.frame(texts))
@@ -0,0 +1,30 @@
1
+ """
2
+ Helpers that deal with Unicode column widths and simple emoji detection.
3
+ """
4
+ import logging
5
+ from wcwidth import wcwidth as _wcwidth
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class WidthUtil:
11
+ """Static helpers – no state required."""
12
+
13
+ @staticmethod
14
+ def wcwidth_narrow(ch: str) -> int:
15
+ """
16
+ Return wcwidth’s value for *ch* (0, 1, or 2).
17
+ Keeping the wrapper separate makes unit-testing easier.
18
+ """
19
+ return _wcwidth(ch)
20
+
21
+ @classmethod
22
+ def is_emoji_line(cls, text: str) -> bool:
23
+ """
24
+ True if any code-point in *text* takes two columns (typical for emoji).
25
+ """
26
+ for ch in text:
27
+ if cls.wcwidth_narrow(ch) > 1:
28
+ logger.info("%r β†’ is emoji", text)
29
+ return True
30
+ return False
haraka/art/create.py ADDED
@@ -0,0 +1,17 @@
1
+ from haraka.art import TextFramer
2
+
3
+ class Create:
4
+ @staticmethod
5
+ def emoji(art: list[str], align: str = "left") -> None:
6
+ frame = TextFramer(border_char_x="", border_char_y="", padding=0, align=align)
7
+ frame.generate(art)
8
+
9
+ @staticmethod
10
+ def ascii(art: list[str], align: str = "left") -> None:
11
+ frame = TextFramer(border_char_x="=", border_char_y="||", padding=2, align=align)
12
+ frame.generate(art)
13
+
14
+ @staticmethod
15
+ def logo(art: list[str], align: str = "left") -> None:
16
+ frame = TextFramer(border_char_x="", border_char_y="", padding=2, align=align)
17
+ frame.generate(art)
File without changes
@@ -0,0 +1,3 @@
1
+ from .config import PostGenConfig
2
+
3
+ __all__ = ["PostGenConfig"]
@@ -0,0 +1,11 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+
4
+ @dataclass(frozen=True, slots=True)
5
+ class PostGenConfig:
6
+ language: str
7
+ project_slug: str
8
+ author: str
9
+ description: str
10
+ project_dir: Path
11
+ swagger: bool
@@ -0,0 +1,54 @@
1
+ from post_gen.gen_ascii.ascii_art import goLang
2
+
3
+ from haraka.art.create import Create
4
+ from haraka.art.ascii.assets import *
5
+ from .config import PostGenConfig
6
+ from haraka.utils import divider, Logger
7
+ from haraka.post_gen.utils.command import CommandRunner
8
+ from haraka.post_gen.utils.files import FileOps
9
+ from haraka.post_gen.utils.purge import ResourcePurger
10
+ from haraka.post_gen.utils.gitops import GitOps
11
+
12
+
13
+ def main(cfg: PostGenConfig) -> None:
14
+
15
+ _logger = Logger(cfg.language)
16
+ logger = _logger.start_logger()
17
+
18
+ try:
19
+ cmd = CommandRunner(logger)
20
+ fops = FileOps(logger)
21
+ purge = ResourcePurger(fops, logger)
22
+ git = GitOps(cmd, logger)
23
+ except Exception as e:
24
+ logger.error(f"Failed to initialize components: {e}")
25
+ raise
26
+
27
+ divider("1️⃣ / 4️⃣ – Purge template junk")
28
+ purge.purge(cfg.language, cfg.project_dir)
29
+
30
+ divider("2️⃣ / 4️⃣ – Initialise Git repo")
31
+ git.init_repo(cfg.project_dir)
32
+
33
+ divider("3️⃣ / 4️⃣ – Commit scaffold")
34
+ git.stage_commit(cfg.project_dir)
35
+
36
+ divider("4️⃣ / 4️⃣ – Create GitHub repo & push")
37
+ git.push_to_github(cfg.project_dir, cfg.author, cfg.project_slug, cfg.description)
38
+
39
+ divider("πŸŽ‰ Project generation complete πŸŽ‰")
40
+
41
+ if cfg.language == "go" and not cfg.swagger:
42
+ go_emoji_logo = [emoji.go]
43
+ go_performance_mode = [
44
+ goLang, divider_xl, performance_mode, divider_l, tools, divider_s,
45
+ gRPC, divider_mono, protoC, divider_mono, autoMaxProcs, divider_mono,
46
+ ants, divider_mono, zeroLog,
47
+ ]
48
+ go_fast = [
49
+ goFast, gRpc_ProtoBuf, server,
50
+ by, wjb_dev
51
+ ]
52
+ Create.emoji(go_emoji_logo)
53
+ Create.ascii(go_performance_mode)
54
+ Create.logo(go_fast)
File without changes
@@ -0,0 +1,204 @@
1
+ # assets.py ← drop this next to utils/, purge.py, etc.
2
+ from haraka.art.ascii.assets import *
3
+
4
+ LANGUAGE_ASSETS = [
5
+ {
6
+ "language": "go",
7
+ "files": [
8
+ "Dockerfile",
9
+ "Makefile",
10
+ "Makefile.cpp",
11
+ "README.md",
12
+ "go.mod",
13
+ "skaffold.yaml",
14
+ "configs/dev.yaml",
15
+ "configs/prod.yaml",
16
+ "internal/config/config.go",
17
+ "internal/handler/echo.go",
18
+ "internal/handler/ping.go",
19
+ "internal/server/health.go",
20
+ "internal/server/server.go",
21
+ "pkg/proto/v1/ping.proto",
22
+ "pkg/proto/v1/service.proto",
23
+ # "cmd/{{ cookiecutter.project_slug }}/main.go",
24
+ "test/integration/echo_integration_test.go",
25
+ "test/integration/health_integration_test.go",
26
+ "test/integration/ping_integration_test.go",
27
+ "test/integration/testutil.go",
28
+ "test/unit/config/config_test.go",
29
+ "test/unit/handler/echo_test.go",
30
+ "test/unit/handler/ping_test.go",
31
+ "test/unit/server/health_test.go",
32
+ "test/unit/server/server_test.go",
33
+ "test/unit_test.go",
34
+ "test/package.go",
35
+ "runConfigurations/go/Golang.run.xml",
36
+ "chart/Chart.yaml",
37
+ "chart/templates/deployment.yaml",
38
+ "chart/templates/service.yaml",
39
+ "chart/templates/_helpers.tpl",
40
+ "chart/values.yaml",
41
+ "infra/terraform/main.tf",
42
+ "src/cpp/CMakeLists.txt",
43
+ "src/cpp/main.cc",
44
+ "tests/cpp/test_stub.cc"
45
+ ],
46
+ "dirs": [
47
+ "cmd/",
48
+ "configs/",
49
+ "internal/",
50
+ "internal/config/",
51
+ "internal/handler/",
52
+ "internal/server/",
53
+ "pkg/",
54
+ "pkg/proto/",
55
+ "pkg/proto/v1/",
56
+ "test/",
57
+ "test/integration/",
58
+ "test/unit/",
59
+ "test/unit/config/",
60
+ "test/unit/handler/",
61
+ "test/unit/server/",
62
+ "runConfigurations/",
63
+ "runConfigurations/go/",
64
+ "chart/",
65
+ "chart/templates/",
66
+ "infra/",
67
+ "infra/terraform/",
68
+ "src/",
69
+ "src/cpp/",
70
+ "tests/",
71
+ "tests/cpp/"
72
+ ]
73
+ },
74
+ {
75
+ "language": "python",
76
+ "files": [
77
+ "Dockerfile",
78
+ "Makefile",
79
+ "README.md",
80
+ "requirements.txt",
81
+ "pytest.ini",
82
+ "skaffold.yaml",
83
+ "src/app/__init__.py",
84
+ "src/app/main.py",
85
+ "src/app/core/config.py",
86
+ "src/app/services/health.py",
87
+ "src/app/schemas/health.py",
88
+ "src/app/api/v1/__init__.py",
89
+ "src/app/api/v1/routers/health.py",
90
+ "tests/__init__.py",
91
+ "tests/conftest.py",
92
+ "tests/integration/test_health_endpoint.py",
93
+ "tests/unit/api/v1/routers/test_health.py",
94
+ "tests/unit/schemas/test_health_schema.py"
95
+ ],
96
+ "dirs": [
97
+ "src/",
98
+ "src/app/",
99
+ "src/app/core/",
100
+ "src/app/services/",
101
+ "src/app/schemas/",
102
+ "src/app/api/",
103
+ "src/app/api/v1/",
104
+ "src/app/api/v1/routers/",
105
+ "tests/",
106
+ "tests/integration/",
107
+ "tests/unit/",
108
+ "tests/unit/api/",
109
+ "tests/unit/api/v1/",
110
+ "tests/unit/api/v1/routers/",
111
+ "tests/unit/schemas/"
112
+ ]
113
+ },
114
+ {
115
+ "language": "java",
116
+ "files": [
117
+ "Dockerfile",
118
+ "Makefile",
119
+ "README.md",
120
+ "pom.xml",
121
+ "skaffold.yaml",
122
+ "src/main/java/com/example/Application.java",
123
+ "src/main/java/com/example/config/OpenApiConfig.java",
124
+ "src/main/java/com/example/controller/HealthController.java",
125
+ "src/main/java/com/example/dto/HealthResponse.java",
126
+ "src/main/java/com/example/exception/GlobalExceptionHandler.java",
127
+ "src/main/java/com/example/service/HealthService.java",
128
+ "src/main/resources/application.yml",
129
+ "src/test/java/com/example/controller/HealthControllerTest.java",
130
+ "src/test/java/com/example/service/HealthServiceTest.java"
131
+ ],
132
+ "dirs": [
133
+ "src/",
134
+ "src/main/",
135
+ "src/main/java/",
136
+ "src/main/java/com/",
137
+ "src/main/java/com/example/",
138
+ "src/main/java/com/example/config/",
139
+ "src/main/java/com/example/controller/",
140
+ "src/main/java/com/example/dto/",
141
+ "src/main/java/com/example/exception/",
142
+ "src/main/java/com/example/service/",
143
+ "src/main/resources/",
144
+ "src/test/",
145
+ "src/test/java/",
146
+ "src/test/java/com/",
147
+ "src/test/java/com/example/",
148
+ "src/test/java/com/example/controller/",
149
+ "src/test/java/com/example/service/"
150
+ ]
151
+ }
152
+ ]
153
+
154
+ # ------------------------------------------------------------------ #
155
+ # GLOBAL ASSETS (kept for every language) #
156
+ # ------------------------------------------------------------------ #
157
+ GLOBAL_ASSETS = {
158
+ "files": [
159
+ "Dockerfile",
160
+ "Makefile",
161
+ "README.md",
162
+ "skaffold.yaml",
163
+ "chart/Chart.yaml",
164
+ "chart/values.yaml",
165
+ "chart/templates/_helpers.tpl",
166
+ "chart/templates/deployment.yaml",
167
+ "chart/templates/service.yaml",
168
+ "infra/terraform/main.tf",
169
+ ],
170
+ "dirs": [
171
+ "chart/",
172
+ "chart/templates/",
173
+ "infra/",
174
+ "infra/terraform/",
175
+ ],
176
+ }
177
+
178
+ go_emoji_logo = [
179
+ emoji["go"]
180
+ ]
181
+ go_performance_mode = [
182
+ goLang,
183
+ divider_xl,
184
+ performance_mode,
185
+ divider_l,
186
+ tools,
187
+ divider_s,
188
+ gRPC,
189
+ divider_mono,
190
+ protoC,
191
+ divider_mono,
192
+ autoMaxProcs,
193
+ divider_mono,
194
+ ants,
195
+ divider_mono,
196
+ zeroLog,
197
+ ]
198
+ go_fast = [
199
+ goFast,
200
+ gRpc_ProtoBuf,
201
+ server,
202
+ by,
203
+ wjb_dev
204
+ ]
@@ -0,0 +1,49 @@
1
+ import subprocess, sys
2
+ from pathlib import Path
3
+ from typing import List, Optional
4
+ from haraka.utils import Logger
5
+
6
+ class CommandRunner:
7
+
8
+ """Thin wrapper around subprocess.run with logging & graceful error-handling."""
9
+
10
+ def __init__(self, logger: Logger) -> None:
11
+ self._log = logger
12
+
13
+ def run(
14
+ self,
15
+ cmd: List[str],
16
+ *,
17
+ cwd: Optional[Path] = None,
18
+ check: bool = True,
19
+ ) -> Optional[subprocess.CompletedProcess]:
20
+ cmd_str = " ".join(cmd)
21
+ self._log.info(f"Running: {cmd_str}")
22
+ try:
23
+ result = subprocess.run(
24
+ cmd,
25
+ cwd=str(cwd) if cwd else None,
26
+ check=check,
27
+ stdout=subprocess.PIPE,
28
+ stderr=subprocess.PIPE,
29
+ text=True,
30
+ )
31
+ if result.stdout:
32
+ self._log.info(f"stdout:\n{result.stdout.strip()}")
33
+ if result.stderr:
34
+ self._log.warn(f"stderr:\n{result.stderr.strip()}", file=sys.stderr)
35
+ return result
36
+ except subprocess.CalledProcessError as e:
37
+ self._log.error(f"command failed: ({cmd_str})", file=sys.stderr)
38
+ if e.stdout:
39
+ self._log.error(f"stdout:\n{e.stdout.strip()}", file=sys.stderr)
40
+ if e.stderr:
41
+ self._log.error(f"stderr:\n{e.stderr.strip()}", file=sys.stderr)
42
+ if check:
43
+ sys.exit(e.returncode)
44
+ return None
45
+ except FileNotFoundError:
46
+ self._log.error(f"command not found: {cmd[0]}", file=sys.stderr)
47
+ if check:
48
+ sys.exit(1)
49
+ return None
@@ -0,0 +1,46 @@
1
+ import shutil
2
+ from pathlib import Path
3
+ from haraka.utils import Logger
4
+
5
+ class FileOps:
6
+ """Filesystem helpers: remove files/dirs, print tree, nice dividers."""
7
+ def __init__(self, logger: Logger = Logger("βš™οΈοΈοΈοΈοΈοΈοΈοΈοΈοΈοΈοΈTestingβš™οΈ"), test_mode=False) -> None:
8
+ self.logger = logger
9
+ self.test_mode = test_mode
10
+
11
+ def _relpath(self, path: Path) -> str:
12
+ try:
13
+ return str(path.relative_to(Path.cwd()))
14
+ except ValueError:
15
+ return str(path) if self.test_mode else f"<non-project-path>: {path}"
16
+
17
+ def remove_file(self, path: Path) -> None:
18
+ try:
19
+ if path.is_file():
20
+ path.unlink()
21
+ self.logger.info(f"Removed file: {self._relpath(path)}")
22
+ elif path.is_dir():
23
+ shutil.rmtree(path, ignore_errors=True)
24
+ self.logger.info(f"Removed directory (expected file): {self._relpath(path)}")
25
+ except Exception as e:
26
+ self.logger.warn(f"Could not remove {self._relpath(path)}: {e}")
27
+
28
+ def remove_dir(self, path: Path) -> None:
29
+ if path.exists():
30
+ try:
31
+ shutil.rmtree(path, ignore_errors=True)
32
+ self.logger.info(f"Removed directory: {self._relpath(path)}")
33
+ except Exception as e:
34
+ self.logger.warn(f"Could not remove directory {self._relpath(path)}: {e}")
35
+
36
+ def print_tree(self, path: Path, prefix: str = "") -> None:
37
+ if not path.exists():
38
+ self.logger.warn(f"Path does not exist: {path}")
39
+ return
40
+ entries = sorted(path.iterdir(), key=lambda p: (p.is_file(), p.name.lower()))
41
+ for i, entry in enumerate(entries):
42
+ branch = "└── " if i == len(entries) - 1 else "β”œβ”€β”€ "
43
+ print(prefix + branch + entry.name)
44
+ if entry.is_dir():
45
+ ext = " " if i == len(entries) - 1 else "β”‚ "
46
+ self.print_tree(entry, prefix + ext)
@@ -0,0 +1,60 @@
1
+ import shutil
2
+ import sys
3
+ from pathlib import Path
4
+ from .command import CommandRunner
5
+ import subprocess
6
+ from haraka.utils import Logger
7
+
8
+ class GitOps:
9
+ """Git-related operations: init, commit, create remote, push."""
10
+
11
+ def __init__(self, runner: CommandRunner, logger: Logger) -> None:
12
+ self._r = runner
13
+ self._log = logger
14
+
15
+ # ------------- public API ----------------------------------------- #
16
+ def init_repo(self, project_dir: Path) -> None:
17
+ if not (project_dir / ".git").exists():
18
+ self._log.info("Initializing Git repository…")
19
+ self._r.run(["git", "init"], cwd=project_dir)
20
+ self._r.run(["git", "branch", "-M", "main"], cwd=project_dir)
21
+ else:
22
+ self._log.warn(".git already exists; skipping git init.")
23
+
24
+ def stage_commit(self, project_dir: Path) -> None:
25
+ self._log.info("Staging files…")
26
+ self._r.run(["git", "add", "."], cwd=project_dir)
27
+ res = self._r.run(["git", "commit", "-m", "Initial commit"],
28
+ cwd=project_dir, check=False)
29
+ if not res or res.returncode:
30
+ self._log.error("'git commit' failed (maybe nothing to commit); continuing…", file=sys.stderr)
31
+
32
+ def push_to_github(self, project_dir: Path, author: str,
33
+ slug: str, description: str) -> None:
34
+ if not self._has_gh():
35
+ return
36
+ if "origin" in self._current_remotes(project_dir):
37
+ self._log.warn("Remote 'origin' already exists; skipping create.")
38
+ return
39
+ repo = f"{author}/{slug}"
40
+ self._log.info(f"Creating GitHub repo {repo} & pushing…")
41
+ self._r.run([
42
+ "gh", "repo", "create", repo,
43
+ "--public", "--description", description,
44
+ "--source", ".", "--remote", "origin", "--push", "--confirm"
45
+ ], cwd=project_dir)
46
+
47
+ # ------------- internals ------------------------------------------ #
48
+ @staticmethod
49
+ def _current_remotes(project_dir: Path):
50
+ try:
51
+ res = subprocess.run(["git", "remote"], cwd=project_dir,
52
+ check=True, text=True,
53
+ stdout=subprocess.PIPE)
54
+ return [r.strip() for r in res.stdout.splitlines()]
55
+ except subprocess.CalledProcessError:
56
+ return []
57
+
58
+ @staticmethod
59
+ def _has_gh() -> bool:
60
+ return shutil.which("gh") is not None
@@ -0,0 +1,70 @@
1
+ # haraka/post_gen/utils/purge.py
2
+ from pathlib import Path
3
+ from typing import Dict, Set
4
+
5
+ from .files import FileOps
6
+ from haraka.utils import Logger, divider
7
+ from haraka.post_gen.utils.assets import LANGUAGE_ASSETS, GLOBAL_ASSETS
8
+
9
+
10
+ class ResourcePurger:
11
+ """Delete template artefacts not relevant to the selected language."""
12
+
13
+ def __init__(self, fops: FileOps, logger: Logger | None = None) -> None:
14
+ self._f = fops
15
+ self._log = logger or Logger("ResourcePurger")
16
+
17
+ # Build a quick lookup: language β†’ {"files": set, "dirs": set}
18
+ self._index: Dict[str, Dict[str, Set[str]]] = {
19
+ spec["language"]: {
20
+ "files": set(spec["files"]),
21
+ # store directory names *without* trailing β€œ/”
22
+ "dirs": {d.rstrip("/") for d in spec["dirs"]},
23
+ }
24
+ for spec in LANGUAGE_ASSETS
25
+ }
26
+
27
+ # ------------------------------------------------------------------ #
28
+ # public API #
29
+ # ------------------------------------------------------------------ #
30
+ def purge(self, language: str, project_dir: Path) -> None:
31
+ language = language.lower()
32
+ if language not in self._index:
33
+ self._log.warn(f"Unrecognised language '{language}'; skipping purge.")
34
+ return
35
+
36
+ self._log.info(f"Starting purge for language: {language}")
37
+
38
+ keep_files = self._index[language]["files"] | set(GLOBAL_ASSETS["files"])
39
+ keep_dirs = self._index[language]["dirs"] | {
40
+ d.rstrip("/") for d in GLOBAL_ASSETS["dirs"]
41
+ }
42
+
43
+ self._log.info(f"Keeping {len(keep_files)} files & {len(keep_dirs)} dirs")
44
+
45
+ self._purge_unrelated(project_dir, keep_files, keep_dirs)
46
+
47
+ divider("Project tree after purge…")
48
+ self._f.print_tree(project_dir)
49
+
50
+ # ------------------------------------------------------------------ #
51
+ # internals #
52
+ # ------------------------------------------------------------------ #
53
+ def _purge_unrelated(
54
+ self,
55
+ root: Path,
56
+ keep_files: Set[str],
57
+ keep_dirs: Set[str],
58
+ ) -> None:
59
+ for path in root.rglob("*"):
60
+ rel = path.relative_to(root).as_posix()
61
+
62
+ inside_kept_dir = any(rel.startswith(d + "/") for d in keep_dirs)
63
+
64
+ if rel in keep_files or rel in keep_dirs or inside_kept_dir:
65
+ continue # safe
66
+
67
+ if path.is_dir():
68
+ self._f.remove_dir(path)
69
+ else:
70
+ self._f.remove_file(path)
@@ -0,0 +1,5 @@
1
+ from haraka.post_gen.config import PostGenConfig
2
+ from .logging.log_util import Logger
3
+ from .common.utils import *
4
+ from haraka.post_gen.utils.assets import *
5
+ __all__ = ["PostGenConfig", "", "divider", "Logger", "LANGUAGE_ASSETS", "GLOBAL_ASSETS"]
File without changes
@@ -0,0 +1,14 @@
1
+ import os
2
+
3
+ def _term_width() -> int:
4
+ try:
5
+ return os.get_terminal_size().columns
6
+ except OSError:
7
+ return 60
8
+
9
+ # -------- pretty printing ------------------------------------------ #
10
+ def divider(title: str, *, char: str = "=") -> None:
11
+ width = _term_width()
12
+ print("\n" + char * width)
13
+ print(title)
14
+ print(char * width + "\n")
File without changes
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+ import sys
3
+ from typing import TextIO
4
+
5
+
6
+ class Logger:
7
+ def __init__(self, label: str = ""):
8
+ self.label = label
9
+
10
+ def start_logger(self) -> Logger:
11
+ label = Logger.get_label(self.label)
12
+ return Logger(label)
13
+
14
+ def info(self, msg: str) -> None:
15
+ print(f"{self.label} INFO: {msg}")
16
+
17
+ def warn(self, msg: str, file: TextIO = sys.stderr) -> None:
18
+ print(f"{self.label} WARNING: {msg}", file=file)
19
+
20
+ def error(self, msg: str, file: TextIO = sys.stderr) -> None:
21
+ print(f"{self.label} ERROR: {msg}", file=file)
22
+
23
+ @staticmethod
24
+ def get_label(language: str) -> str:
25
+ if language == "go":
26
+ return f"[πŸ”₯Go Fast: post_gen]"
27
+ return f"[πŸ”₯post_gen ({language})]"
28
+
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: haraka
3
+ Version: 0.2.5
4
+ Summary: Reusable post-generation helper for Cookiecutter micro-service templates
5
+ Author-email: Will Burks <will@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/wjb-dev/comet-postgen
8
+ Project-URL: Source, https://github.com/wjb-dev/comet-postgen
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
@@ -0,0 +1,28 @@
1
+ haraka/__init__.py,sha256=oQxsY6uhvvb8bUdYvbP6oWo48gfzJLunsYTVmFFSKBs,176
2
+ haraka/art/__init__.py,sha256=-b1KHQiLkaI2sXaHS05XDaE27tZfXvVLitILjJfSiKs,112
3
+ haraka/art/create.py,sha256=D2VPJtuwI0Ed7CcreHRHhxSN1VzV7PQfC1q-NW8l0DY,631
4
+ haraka/art/ascii/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ haraka/art/ascii/assets.py,sha256=1WUxFQ0xlzaScVe6hXh6gHKggtLhYc3gfHi9CoW9Otc,9159
6
+ haraka/art/ascii/frame/__init__.py,sha256=9Y1qWC-82ZK_kLhRq749hh_3tXb0mMaPkHq70jtElTc,156
7
+ haraka/art/ascii/frame/border.py,sha256=n7qaLxzzBmf8ewatbe5gN97DO4afZM67bBjpU1OqgAM,1318
8
+ haraka/art/ascii/frame/framer.py,sha256=s_-lsb-hGhlH_73q5iTtk8KHW0MSeSHSAvNFVmL1c9A,3951
9
+ haraka/art/ascii/frame/width_utils.py,sha256=in4AV3gTBBXUX6N01j67Icu9vsHFomybBN8rpL3sQ5s,810
10
+ haraka/post_gen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ haraka/post_gen/runner.py,sha256=nKVprtGZzVJBhRwU4vDV6op2d63RQ30BEX6XinuSdIY,1813
12
+ haraka/post_gen/config/__init__.py,sha256=-_XkJ_Dni28yJCMfIceQSiH1wa_hHsZMoBTyvR9Ikbc,62
13
+ haraka/post_gen/config/config.py,sha256=VpRgp_-dU1sJd3Jow3MAOeerqElzBcHdkSqvgd1bkwk,234
14
+ haraka/post_gen/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ haraka/post_gen/utils/assets.py,sha256=UARi1iDczVHoNGm5FpKkSXCk-yvnzyMMtSJM1SxMRSA,6157
16
+ haraka/post_gen/utils/command.py,sha256=5yxPSQmQpzAyPNoLtzxaUz1VWgPnTfD7XlkBUu5rIeI,1672
17
+ haraka/post_gen/utils/files.py,sha256=1hPHn_nYI5A2JKJqxmNFK5fpo9DNPcGR4K7qCPOO5GE,1993
18
+ haraka/post_gen/utils/gitops.py,sha256=hRnztg_5yW3L_bdh05gcgGQ25wwjSTn8pX1sja-m91s,2434
19
+ haraka/post_gen/utils/purge.py,sha256=9EQg-CdFVnjEo-KY9_Au_emicpZId7z2-QyWFPLty-M,2633
20
+ haraka/utils/__init__.py,sha256=dqjKz10MTfH6fFl68cu_giWYYZ41HAUDg_0-OwC1qjE,246
21
+ haraka/utils/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ haraka/utils/common/utils.py,sha256=kMnMXe_hcxGkD0MKGmR1lIwsRND7BaFPRbGN4PwonfM,360
23
+ haraka/utils/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ haraka/utils/logging/log_util.py,sha256=rl-VnSPns84MPDp7SwArtrO4oPHUPUiRNdkp0NBvxsY,778
25
+ haraka-0.2.5.dist-info/METADATA,sha256=hgLAOgD2_WhGfof6xrY1U5k5u0GuIvnZfODD9g6uc_c,578
26
+ haraka-0.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ haraka-0.2.5.dist-info/top_level.txt,sha256=1khpwypLKWoklVd_CgFiwAfcctVSXRoRPc3BI9lyIXo,7
28
+ haraka-0.2.5.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ haraka