passiveaggressive 0.1.0__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.
- passiveaggressive/__init__.py +21 -0
- passiveaggressive/cli.py +64 -0
- passiveaggressive/py.typed +0 -0
- passiveaggressive/templates.py +307 -0
- passiveaggressive/translator.py +134 -0
- passiveaggressive/utils.py +64 -0
- passiveaggressive-0.1.0.dist-info/METADATA +220 -0
- passiveaggressive-0.1.0.dist-info/RECORD +12 -0
- passiveaggressive-0.1.0.dist-info/WHEEL +5 -0
- passiveaggressive-0.1.0.dist-info/entry_points.txt +2 -0
- passiveaggressive-0.1.0.dist-info/licenses/LICENSE +21 -0
- passiveaggressive-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""passiveaggressive — turn polite messages into subtly passive-aggressive ones.
|
|
2
|
+
|
|
3
|
+
>>> from passiveaggressive import translate
|
|
4
|
+
>>> translate("Please update the documentation.", seed=1) # doctest: +ELLIPSIS
|
|
5
|
+
'...documentation...'
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .templates import INTENSITIES, STYLES
|
|
9
|
+
from .translator import email, rewrite, signoff, subject, translate
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"translate",
|
|
13
|
+
"rewrite",
|
|
14
|
+
"email",
|
|
15
|
+
"subject",
|
|
16
|
+
"signoff",
|
|
17
|
+
"STYLES",
|
|
18
|
+
"INTENSITIES",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
passiveaggressive/cli.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Command-line entry point. Installed as the ``passiveaggressive`` script.
|
|
2
|
+
|
|
3
|
+
Run ``passiveaggressive --help`` for the full rundown.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .templates import INTENSITIES, STYLES
|
|
12
|
+
from .translator import translate
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="passiveaggressive",
|
|
18
|
+
description="Convert a polite message into a subtly passive-aggressive one.",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument("text", help="The original, polite message to transform.")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--style",
|
|
23
|
+
choices=STYLES,
|
|
24
|
+
default="coworker",
|
|
25
|
+
help="Who's supposedly sending this (default: coworker).",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--intensity",
|
|
29
|
+
type=int,
|
|
30
|
+
choices=INTENSITIES,
|
|
31
|
+
default=3,
|
|
32
|
+
help="How annoyed they are, 1-5 (default: 3).",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--seed",
|
|
36
|
+
type=int,
|
|
37
|
+
default=None,
|
|
38
|
+
help="Set this to get the same output every time.",
|
|
39
|
+
)
|
|
40
|
+
return parser
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: list[str] | None = None) -> int:
|
|
44
|
+
"""Parse args, translate, print. Returns an exit code."""
|
|
45
|
+
parser = build_parser()
|
|
46
|
+
args = parser.parse_args(argv)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
result = translate(
|
|
50
|
+
args.text,
|
|
51
|
+
style=args.style,
|
|
52
|
+
intensity=args.intensity,
|
|
53
|
+
seed=args.seed,
|
|
54
|
+
)
|
|
55
|
+
except (TypeError, ValueError) as exc:
|
|
56
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
|
|
59
|
+
print(result)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""All the phrase banks live here.
|
|
2
|
+
|
|
3
|
+
Everything's hand-written and organized by ``style`` (who's supposedly
|
|
4
|
+
sending the message) and ``intensity`` (how annoyed they are). If you
|
|
5
|
+
want to add a new style or tweak the wording, this is the only file
|
|
6
|
+
you need to touch — the logic in translator.py doesn't care what the
|
|
7
|
+
phrases actually say. Keep it office-friendly: mildly annoyed, never
|
|
8
|
+
actually mean.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Final
|
|
14
|
+
|
|
15
|
+
#: Supported style names.
|
|
16
|
+
STYLES: Final[tuple[str, ...]] = (
|
|
17
|
+
"coworker",
|
|
18
|
+
"manager",
|
|
19
|
+
"customer_support",
|
|
20
|
+
"british",
|
|
21
|
+
"parent",
|
|
22
|
+
"teacher",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
#: Supported intensity levels (1 = barely-there politeness, 5 = maximum sarcasm).
|
|
26
|
+
INTENSITIES: Final[tuple[int, ...]] = (1, 2, 3, 4, 5)
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Prefixes — the opening clause that sets the tone. style -> intensity -> options.
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
PREFIXES: Final[dict[str, dict[int, tuple[str, ...]]]] = {
|
|
33
|
+
"coworker": {
|
|
34
|
+
1: (
|
|
35
|
+
"Just a gentle reminder,",
|
|
36
|
+
"Whenever you have a chance,",
|
|
37
|
+
"Quick note,",
|
|
38
|
+
"Just popping this back up,",
|
|
39
|
+
),
|
|
40
|
+
2: (
|
|
41
|
+
"Just checking in,",
|
|
42
|
+
"Circling back on this,",
|
|
43
|
+
"Not to nag, but",
|
|
44
|
+
"Following up here,",
|
|
45
|
+
),
|
|
46
|
+
3: (
|
|
47
|
+
"Just following up since this seems to have been overlooked,",
|
|
48
|
+
"Bumping this one more time,",
|
|
49
|
+
"Not sure if this got buried, but",
|
|
50
|
+
"Coming back to this again,",
|
|
51
|
+
),
|
|
52
|
+
4: (
|
|
53
|
+
"Hate to keep bringing this up, but",
|
|
54
|
+
"This is now my third time mentioning it, and",
|
|
55
|
+
"Just so it doesn't get lost again,",
|
|
56
|
+
"Following up, as this seems to keep slipping,",
|
|
57
|
+
),
|
|
58
|
+
5: (
|
|
59
|
+
"No rush at all, as always, but",
|
|
60
|
+
"Truly in no hurry, yet somehow still asking,",
|
|
61
|
+
"Not that it matters at this point, but",
|
|
62
|
+
"Just a friendly nudge for the fourth time,",
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
"manager": {
|
|
66
|
+
1: (
|
|
67
|
+
"Whenever you get a moment,",
|
|
68
|
+
"Just wanted to touch base,",
|
|
69
|
+
"For your awareness,",
|
|
70
|
+
"Just a heads up,",
|
|
71
|
+
),
|
|
72
|
+
2: (
|
|
73
|
+
"Just checking in on this again since I haven't seen an update,",
|
|
74
|
+
"Wanted to follow up on where this stands,",
|
|
75
|
+
"Circling back for visibility,",
|
|
76
|
+
"Touching base once more,",
|
|
77
|
+
),
|
|
78
|
+
3: (
|
|
79
|
+
"I'll need an update on this soon,",
|
|
80
|
+
"This is still pending on my end,",
|
|
81
|
+
"Wanted to flag that this hasn't moved,",
|
|
82
|
+
"Coming back around on this open item,",
|
|
83
|
+
),
|
|
84
|
+
4: (
|
|
85
|
+
"I've mentioned this a couple of times now, and",
|
|
86
|
+
"This continues to sit without an update, so",
|
|
87
|
+
"I'd appreciate some movement on this, since",
|
|
88
|
+
"Raising this again, as",
|
|
89
|
+
),
|
|
90
|
+
5: (
|
|
91
|
+
"I'm sure it's just an oversight, but",
|
|
92
|
+
"At this point I've lost count of the follow-ups, and",
|
|
93
|
+
"Purely for my own curiosity,",
|
|
94
|
+
"Not to escalate, but I may have to, because",
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
"customer_support": {
|
|
98
|
+
1: (
|
|
99
|
+
"Thank you for your patience,",
|
|
100
|
+
"We just wanted to let you know,",
|
|
101
|
+
"As a quick update,",
|
|
102
|
+
"We're reaching out to confirm,",
|
|
103
|
+
),
|
|
104
|
+
2: (
|
|
105
|
+
"We wanted to follow up on your request,",
|
|
106
|
+
"Just checking in regarding your ticket,",
|
|
107
|
+
"We noticed this is still open, so",
|
|
108
|
+
"For your reference,",
|
|
109
|
+
),
|
|
110
|
+
3: (
|
|
111
|
+
"We understand this may have been missed, so",
|
|
112
|
+
"We're following up once more on this,",
|
|
113
|
+
"This ticket remains unresolved, so",
|
|
114
|
+
"We wanted to gently revisit this,",
|
|
115
|
+
),
|
|
116
|
+
4: (
|
|
117
|
+
"We've reached out a few times regarding this, and",
|
|
118
|
+
"This request continues to be open, so",
|
|
119
|
+
"We remain hopeful for a resolution, as",
|
|
120
|
+
"For the record, we have followed up before, and",
|
|
121
|
+
),
|
|
122
|
+
5: (
|
|
123
|
+
"We're absolutely not in a rush, however",
|
|
124
|
+
"We do value your business, which is why",
|
|
125
|
+
"This ticket has aged gracefully, so",
|
|
126
|
+
"Per our multiple previous emails,",
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
"british": {
|
|
130
|
+
1: (
|
|
131
|
+
"Not to be a bother, but",
|
|
132
|
+
"Terribly sorry to bring this up, but",
|
|
133
|
+
"Just a small thought, but",
|
|
134
|
+
"If it's no trouble,",
|
|
135
|
+
),
|
|
136
|
+
2: (
|
|
137
|
+
"I do hope you don't mind me asking again, but",
|
|
138
|
+
"Ever so sorry to follow up, but",
|
|
139
|
+
"This might be a silly question, but",
|
|
140
|
+
"I don't want to be a pain, but",
|
|
141
|
+
),
|
|
142
|
+
3: (
|
|
143
|
+
"I did wonder if this had perhaps slipped through, so",
|
|
144
|
+
"Frightfully sorry to mention it once more, but",
|
|
145
|
+
"Rather hoping this hasn't been forgotten, so",
|
|
146
|
+
"Just popping my head round the door to say",
|
|
147
|
+
),
|
|
148
|
+
4: (
|
|
149
|
+
"I do apologize for going on about this, but",
|
|
150
|
+
"Not wishing to labour the point, but",
|
|
151
|
+
"Terribly persistent of me, I know, but",
|
|
152
|
+
"I realise I've asked before, and dreadfully sorry, but",
|
|
153
|
+
),
|
|
154
|
+
5: (
|
|
155
|
+
"I wouldn't dream of making a fuss, but",
|
|
156
|
+
"Do forgive my being ever so slightly tenacious, but",
|
|
157
|
+
"Frightfully embarrassing to keep asking, but",
|
|
158
|
+
"Only mentioning it for the umpteenth time because",
|
|
159
|
+
),
|
|
160
|
+
},
|
|
161
|
+
"parent": {
|
|
162
|
+
1: (
|
|
163
|
+
"Just leaving this here,",
|
|
164
|
+
"No pressure, but",
|
|
165
|
+
"Just a little note,",
|
|
166
|
+
"Whenever you think of it,",
|
|
167
|
+
),
|
|
168
|
+
2: (
|
|
169
|
+
"I'll just leave this here in case anyone remembers,",
|
|
170
|
+
"Not that anyone asked, but",
|
|
171
|
+
"Just saying, in case it matters,",
|
|
172
|
+
"I only mention this because",
|
|
173
|
+
),
|
|
174
|
+
3: (
|
|
175
|
+
"I've said this before, but",
|
|
176
|
+
"It's fine, I'll just do it myself, but",
|
|
177
|
+
"I'm only bringing it up again because",
|
|
178
|
+
"Someone might want to know that",
|
|
179
|
+
),
|
|
180
|
+
4: (
|
|
181
|
+
"It's fine. Really. But",
|
|
182
|
+
"I'm not upset, I just noticed that",
|
|
183
|
+
"No, go ahead, don't worry about it, but",
|
|
184
|
+
"I only ask because I've asked before, and",
|
|
185
|
+
),
|
|
186
|
+
5: (
|
|
187
|
+
"It's totally fine, don't worry about it, but",
|
|
188
|
+
"I'm sure you were just about to get to it, but",
|
|
189
|
+
"No need to explain, but",
|
|
190
|
+
"I won't say I told you so, but",
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
"teacher": {
|
|
194
|
+
1: (
|
|
195
|
+
"A gentle reminder,",
|
|
196
|
+
"Just a friendly note,",
|
|
197
|
+
"For your reference,",
|
|
198
|
+
"As previously discussed,",
|
|
199
|
+
),
|
|
200
|
+
2: (
|
|
201
|
+
"As a reminder from last time,",
|
|
202
|
+
"I wanted to check in on this,",
|
|
203
|
+
"This was mentioned in class, and",
|
|
204
|
+
"Following up on our earlier discussion,",
|
|
205
|
+
),
|
|
206
|
+
3: (
|
|
207
|
+
"This has come up more than once, so",
|
|
208
|
+
"As noted previously,",
|
|
209
|
+
"I'll remind the class again that",
|
|
210
|
+
"This continues to be an outstanding item, so",
|
|
211
|
+
),
|
|
212
|
+
4: (
|
|
213
|
+
"This has been mentioned several times now, so",
|
|
214
|
+
"I do hope this will be addressed, since",
|
|
215
|
+
"Let's revisit this one more time, since",
|
|
216
|
+
"As stated in the syllabus, and again here,",
|
|
217
|
+
),
|
|
218
|
+
5: (
|
|
219
|
+
"I'm sure this was simply an oversight, but",
|
|
220
|
+
"Extra credit for finally addressing this, since",
|
|
221
|
+
"This is now a recurring theme, so",
|
|
222
|
+
"Noted for the permanent record, that",
|
|
223
|
+
),
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Softeners — dropped in mid-sentence to keep things technically polite.
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
SOFTENERS: Final[tuple[str, ...]] = (
|
|
232
|
+
"if possible",
|
|
233
|
+
"whenever you can",
|
|
234
|
+
"when convenient",
|
|
235
|
+
"at your earliest convenience",
|
|
236
|
+
"if time allows",
|
|
237
|
+
"no pressure",
|
|
238
|
+
"whenever works for you",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Closings, keyed by intensity.
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
CLOSINGS: Final[dict[int, tuple[str, ...]]] = {
|
|
246
|
+
1: (
|
|
247
|
+
"Thanks in advance.",
|
|
248
|
+
"Much appreciated.",
|
|
249
|
+
"Hopefully this helps.",
|
|
250
|
+
"Thanks so much.",
|
|
251
|
+
),
|
|
252
|
+
2: (
|
|
253
|
+
"Looking forward to hearing back.",
|
|
254
|
+
"Appreciate your attention to this.",
|
|
255
|
+
"Thanks for understanding.",
|
|
256
|
+
"Let me know how it goes.",
|
|
257
|
+
),
|
|
258
|
+
3: (
|
|
259
|
+
"Hoping to hear something soon.",
|
|
260
|
+
"Appreciate you taking a look.",
|
|
261
|
+
"Thanks for circling back on this one.",
|
|
262
|
+
"Let's get this squared away.",
|
|
263
|
+
),
|
|
264
|
+
4: (
|
|
265
|
+
"Would appreciate an update sooner rather than later.",
|
|
266
|
+
"Hoping this can finally be resolved.",
|
|
267
|
+
"Thanks for finally getting to this.",
|
|
268
|
+
"Let's not let this linger any further.",
|
|
269
|
+
),
|
|
270
|
+
5: (
|
|
271
|
+
"No rush at all, as always.",
|
|
272
|
+
"Whenever this century works for you.",
|
|
273
|
+
"Take your time, clearly no one else is.",
|
|
274
|
+
"Looking forward to this being ancient history.",
|
|
275
|
+
),
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# Extra bits for email() / subject() / signoff().
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
EMAIL_PHRASES: Final[tuple[str, ...]] = (
|
|
283
|
+
"Just following up on this.",
|
|
284
|
+
"As mentioned previously,",
|
|
285
|
+
"Kindly take a look when you can.",
|
|
286
|
+
"Looking forward to hearing from you.",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
SUBJECTS: Final[tuple[str, ...]] = (
|
|
290
|
+
"Following Up (Again)",
|
|
291
|
+
"Gentle Reminder",
|
|
292
|
+
"Quick Check-In",
|
|
293
|
+
"Re: Previous Email",
|
|
294
|
+
"Checking In",
|
|
295
|
+
"Touching Base",
|
|
296
|
+
"Friendly Reminder",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
SIGNOFFS: Final[tuple[str, ...]] = (
|
|
300
|
+
"Kind regards,",
|
|
301
|
+
"Warm regards,",
|
|
302
|
+
"As always,",
|
|
303
|
+
"Many thanks,",
|
|
304
|
+
"Best,",
|
|
305
|
+
"Thanks in advance,",
|
|
306
|
+
"Appreciatively,",
|
|
307
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""The actual translation logic.
|
|
2
|
+
|
|
3
|
+
Everything here is built from the phrase banks in ``templates.py``. No
|
|
4
|
+
AI, no network calls, just a random.Random instance picking from
|
|
5
|
+
handcrafted lists of phrases. If you want to know why a particular
|
|
6
|
+
sentence came out the way it did, that's the module to look at.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .templates import (
|
|
12
|
+
CLOSINGS,
|
|
13
|
+
EMAIL_PHRASES,
|
|
14
|
+
INTENSITIES,
|
|
15
|
+
PREFIXES,
|
|
16
|
+
SIGNOFFS,
|
|
17
|
+
SOFTENERS,
|
|
18
|
+
STYLES,
|
|
19
|
+
SUBJECTS,
|
|
20
|
+
)
|
|
21
|
+
from .utils import capitalize_first, get_rng, normalize_core
|
|
22
|
+
|
|
23
|
+
__all__ = ["translate", "rewrite", "email", "subject", "signoff"]
|
|
24
|
+
|
|
25
|
+
_DEFAULT_STYLE = "coworker"
|
|
26
|
+
_DEFAULT_INTENSITY = 3
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate_style(style: str) -> None:
|
|
30
|
+
if style not in STYLES:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"Unknown style {style!r}. Valid styles are: {', '.join(STYLES)}."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _validate_intensity(intensity: int) -> None:
|
|
37
|
+
if intensity not in INTENSITIES:
|
|
38
|
+
raise ValueError(f"Intensity must be one of {INTENSITIES}, got {intensity!r}.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _validate_text(text: str) -> None:
|
|
42
|
+
if not isinstance(text, str):
|
|
43
|
+
raise TypeError(f"text must be a str, got {type(text).__name__}.")
|
|
44
|
+
if not text.strip():
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"text can't be empty (or just whitespace) — there's nothing to translate."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def translate(
|
|
51
|
+
text: str,
|
|
52
|
+
style: str = _DEFAULT_STYLE,
|
|
53
|
+
intensity: int = _DEFAULT_INTENSITY,
|
|
54
|
+
seed: int | None = None,
|
|
55
|
+
) -> str:
|
|
56
|
+
"""Turn a plain, polite message into a subtly passive-aggressive one.
|
|
57
|
+
|
|
58
|
+
``style`` picks the personality delivering the line (coworker, manager,
|
|
59
|
+
customer_support, british, parent, or teacher — see ``STYLES``), and
|
|
60
|
+
``intensity`` (1-5) controls how much annoyance leaks through. Pass a
|
|
61
|
+
``seed`` if you need the same output twice; leave it off and you'll get
|
|
62
|
+
natural variation on every call.
|
|
63
|
+
|
|
64
|
+
>>> translate("Fix the bug.", seed=1) # doctest: +ELLIPSIS
|
|
65
|
+
'...bug...'
|
|
66
|
+
|
|
67
|
+
Raises ``TypeError`` if ``text`` isn't a string, and ``ValueError`` if
|
|
68
|
+
it's empty, ``style`` isn't recognized, or ``intensity`` is out of range.
|
|
69
|
+
"""
|
|
70
|
+
_validate_text(text)
|
|
71
|
+
_validate_style(style)
|
|
72
|
+
_validate_intensity(intensity)
|
|
73
|
+
|
|
74
|
+
rng = get_rng(seed)
|
|
75
|
+
|
|
76
|
+
core = normalize_core(text)
|
|
77
|
+
prefix = rng.choice(PREFIXES[style][intensity])
|
|
78
|
+
softener = rng.choice(SOFTENERS)
|
|
79
|
+
closing = rng.choice(CLOSINGS[intensity])
|
|
80
|
+
|
|
81
|
+
# A few different sentence shapes so the same prefix/softener/closing
|
|
82
|
+
# combo doesn't always come out reading identically.
|
|
83
|
+
shape = rng.choice(("simple", "softened", "buried"))
|
|
84
|
+
if shape == "simple":
|
|
85
|
+
body = f"{prefix} {core}."
|
|
86
|
+
elif shape == "softened":
|
|
87
|
+
body = f"{prefix} {core}, {softener}."
|
|
88
|
+
else: # buried — tuck the softener in after an em dash
|
|
89
|
+
body = f"{prefix} {core} -- {softener}."
|
|
90
|
+
|
|
91
|
+
return f"{capitalize_first(body)} {closing}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def rewrite(
|
|
95
|
+
text: str,
|
|
96
|
+
style: str = _DEFAULT_STYLE,
|
|
97
|
+
intensity: int = _DEFAULT_INTENSITY,
|
|
98
|
+
seed: int | None = None,
|
|
99
|
+
) -> str:
|
|
100
|
+
"""Alias for ``translate()`` — some people just prefer the verb "rewrite"."""
|
|
101
|
+
return translate(text, style=style, intensity=intensity, seed=seed)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def email(
|
|
105
|
+
text: str,
|
|
106
|
+
style: str = _DEFAULT_STYLE,
|
|
107
|
+
intensity: int = _DEFAULT_INTENSITY,
|
|
108
|
+
seed: int | None = None,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Like ``translate()``, but for email bodies.
|
|
111
|
+
|
|
112
|
+
Runs the text through the normal translation and tacks on an
|
|
113
|
+
email-flavored closer, e.g. "Looking forward to hearing from you."
|
|
114
|
+
"""
|
|
115
|
+
_validate_text(text)
|
|
116
|
+
_validate_style(style)
|
|
117
|
+
_validate_intensity(intensity)
|
|
118
|
+
|
|
119
|
+
rng = get_rng(seed)
|
|
120
|
+
translated = translate(text, style=style, intensity=intensity, seed=seed)
|
|
121
|
+
extra = rng.choice(EMAIL_PHRASES)
|
|
122
|
+
return f"{translated} {extra}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def subject(seed: int | None = None) -> str:
|
|
126
|
+
"""Pick a passive-aggressive email subject line, e.g. "Gentle Reminder"."""
|
|
127
|
+
rng = get_rng(seed)
|
|
128
|
+
return rng.choice(SUBJECTS)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def signoff(seed: int | None = None) -> str:
|
|
132
|
+
"""Pick a passive-aggressive sign-off, e.g. "As always,"."""
|
|
133
|
+
rng = get_rng(seed)
|
|
134
|
+
return rng.choice(SIGNOFFS)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Small text/RNG helpers used by the translator.
|
|
2
|
+
|
|
3
|
+
Nothing fancy — just string cleanup and a thin wrapper around
|
|
4
|
+
random.Random so seeding stays consistent across the package.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import random
|
|
10
|
+
import re
|
|
11
|
+
from typing import Final
|
|
12
|
+
|
|
13
|
+
# Leading imperative phrases we strip off so "Please send the report."
|
|
14
|
+
# reads naturally once it's dropped into a template, e.g. "send the report".
|
|
15
|
+
_LEADING_POLITE_WORDS: Final[tuple[str, ...]] = (
|
|
16
|
+
"please ",
|
|
17
|
+
"kindly ",
|
|
18
|
+
"could you ",
|
|
19
|
+
"can you ",
|
|
20
|
+
"would you ",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_rng(seed: int | None) -> random.Random:
|
|
25
|
+
"""Return a random.Random — seeded if you gave us one, fresh otherwise."""
|
|
26
|
+
return random.Random(seed)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def normalize_core(text: str) -> str:
|
|
30
|
+
"""Turn raw input into a lowercase fragment that fits inside a template.
|
|
31
|
+
|
|
32
|
+
Drops trailing punctuation and a handful of common leading phrases
|
|
33
|
+
("Please", "Could you", ...), then lowercases the first letter.
|
|
34
|
+
|
|
35
|
+
>>> normalize_core("Please update the documentation.")
|
|
36
|
+
'update the documentation'
|
|
37
|
+
>>> normalize_core("Can you send the report?")
|
|
38
|
+
'send the report'
|
|
39
|
+
"""
|
|
40
|
+
stripped = text.strip()
|
|
41
|
+
if not stripped:
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
stripped = re.sub(r"[.?!]+$", "", stripped).strip()
|
|
45
|
+
lowered = stripped[0].lower() + stripped[1:] if stripped else stripped
|
|
46
|
+
|
|
47
|
+
for lead in _LEADING_POLITE_WORDS:
|
|
48
|
+
if lowered.startswith(lead):
|
|
49
|
+
lowered = lowered[len(lead) :]
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
return lowered.strip()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def capitalize_first(text: str) -> str:
|
|
56
|
+
"""Capitalize just the first character, leave everything else as-is."""
|
|
57
|
+
if not text:
|
|
58
|
+
return text
|
|
59
|
+
return text[0].upper() + text[1:]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_unicode_safe(text: str) -> bool:
|
|
63
|
+
"""True if there's actually something in ``text`` besides whitespace."""
|
|
64
|
+
return bool(text.strip())
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: passiveaggressive
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert polite messages into subtly passive-aggressive ones.
|
|
5
|
+
Author: passiveaggressive contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/example/passiveaggressive
|
|
8
|
+
Project-URL: Repository, https://github.com/example/passiveaggressive
|
|
9
|
+
Project-URL: Changelog, https://github.com/example/passiveaggressive/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: humor,text,office,email,cli
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Communications :: Email
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
30
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# passiveaggressive
|
|
34
|
+
|
|
35
|
+
[](https://github.com/example/passiveaggressive/actions/workflows/ci.yml)
|
|
36
|
+
[](https://pypi.org/project/passiveaggressive/)
|
|
37
|
+
[](https://pypi.org/project/passiveaggressive/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
|
|
40
|
+
Convert polite messages into subtly passive-aggressive ones — the kind of
|
|
41
|
+
message you might get from an overly polite coworker, manager, or customer
|
|
42
|
+
support rep. Funny without being mean, office-friendly, and built entirely
|
|
43
|
+
from handcrafted phrase templates — no AI, no network calls, no runtime
|
|
44
|
+
dependencies.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
>>> from passiveaggressive import translate
|
|
48
|
+
>>> translate("Please update the documentation.") # one possible output
|
|
49
|
+
'Just a gentle reminder, update the documentation, if time allows. Thanks in advance.'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install passiveaggressive
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or, for local development:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/example/passiveaggressive.git
|
|
62
|
+
cd passiveaggressive
|
|
63
|
+
pip install -e ".[dev]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Requires Python 3.10+.
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### Basic translation
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from passiveaggressive import translate
|
|
74
|
+
|
|
75
|
+
translate("Send me the report.")
|
|
76
|
+
# "Whenever you have a moment, it would be appreciated if the report
|
|
77
|
+
# could finally be sent over."
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Styles
|
|
81
|
+
|
|
82
|
+
Choose the personality delivering the message:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
translate(text, style="coworker") # default
|
|
86
|
+
translate(text, style="manager")
|
|
87
|
+
translate(text, style="customer_support")
|
|
88
|
+
translate(text, style="british")
|
|
89
|
+
translate(text, style="parent")
|
|
90
|
+
translate(text, style="teacher")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| Style | Example output |
|
|
94
|
+
|-------------------|-----------------|
|
|
95
|
+
| `coworker` | "Just circling back on the PR. Thanks in advance." |
|
|
96
|
+
| `manager` | "Just checking in on this again since I haven't seen an update." |
|
|
97
|
+
| `customer_support`| "We wanted to follow up on your request. Much appreciated." |
|
|
98
|
+
| `british` | "Not to be a bother, but I was wondering whether this might be looked at when convenient." |
|
|
99
|
+
| `parent` | "I'll just leave this here in case anyone remembers." |
|
|
100
|
+
| `teacher` | "A gentle reminder, this was mentioned in class." |
|
|
101
|
+
|
|
102
|
+
### Intensity
|
|
103
|
+
|
|
104
|
+
Dial the annoyance from 1 (barely-there politeness) to 5 (maximum office
|
|
105
|
+
sarcasm):
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
translate(text, intensity=1) # "Whenever you have a chance..."
|
|
109
|
+
translate(text, intensity=3) # "Just following up since this seems to have been overlooked."
|
|
110
|
+
translate(text, intensity=5) # "No rush at all—as always—but it would be wonderful if this
|
|
111
|
+
# could happen sometime this century."
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Randomization & determinism
|
|
115
|
+
|
|
116
|
+
Repeated calls vary naturally:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
translate("Please review the PR.")
|
|
120
|
+
# "Just circling back on the PR."
|
|
121
|
+
# "Thought I'd mention this again in case it slipped through."
|
|
122
|
+
# "Whenever convenient, the PR is still waiting."
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Pass a `seed` for reproducible output:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
translate("Please review the PR.", seed=42)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Extra functions
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from passiveaggressive import rewrite, email, subject, signoff
|
|
135
|
+
|
|
136
|
+
rewrite(text) # alias of translate()
|
|
137
|
+
email(text) # adds email-specific phrasing
|
|
138
|
+
subject() # e.g. "Gentle Reminder", "Following Up (Again)"
|
|
139
|
+
signoff() # e.g. "Kind regards,", "As always,"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## CLI
|
|
143
|
+
|
|
144
|
+
Installing the package also installs the `passiveaggressive` command:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
$ passiveaggressive "Please send the invoice."
|
|
148
|
+
Just checking in regarding the invoice whenever convenient.
|
|
149
|
+
|
|
150
|
+
$ passiveaggressive "Fix the bug" --style british --intensity 4
|
|
151
|
+
$ passiveaggressive "Reply to my email" --seed 10
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Flags:
|
|
155
|
+
|
|
156
|
+
- `--style {coworker,manager,customer_support,british,parent,teacher}`
|
|
157
|
+
- `--intensity {1,2,3,4,5}`
|
|
158
|
+
- `--seed <int>`
|
|
159
|
+
|
|
160
|
+
## API documentation
|
|
161
|
+
|
|
162
|
+
### `translate(text, style="coworker", intensity=3, seed=None) -> str`
|
|
163
|
+
|
|
164
|
+
Transforms `text` into a passive-aggressive message.
|
|
165
|
+
|
|
166
|
+
- `text` (`str`, required) — the original message. Must be non-empty.
|
|
167
|
+
- `style` (`str`) — one of the six supported styles.
|
|
168
|
+
- `intensity` (`int`) — 1–5.
|
|
169
|
+
- `seed` (`int | None`) — optional seed for deterministic output.
|
|
170
|
+
|
|
171
|
+
Raises `TypeError` if `text` isn't a string, and `ValueError` if `text` is
|
|
172
|
+
empty/whitespace, `style` is unrecognized, or `intensity` is out of range.
|
|
173
|
+
|
|
174
|
+
### `rewrite(text, style="coworker", intensity=3, seed=None) -> str`
|
|
175
|
+
|
|
176
|
+
Alias of `translate()`.
|
|
177
|
+
|
|
178
|
+
### `email(text, style="coworker", intensity=3, seed=None) -> str`
|
|
179
|
+
|
|
180
|
+
Like `translate()`, but layers in additional email-specific phrasing
|
|
181
|
+
(e.g. "Looking forward to hearing from you.").
|
|
182
|
+
|
|
183
|
+
### `subject(seed=None) -> str`
|
|
184
|
+
|
|
185
|
+
Generates a passive-aggressive email subject line.
|
|
186
|
+
|
|
187
|
+
### `signoff(seed=None) -> str`
|
|
188
|
+
|
|
189
|
+
Generates a passive-aggressive email signoff.
|
|
190
|
+
|
|
191
|
+
## Random examples
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
"Just circling back on the PR."
|
|
195
|
+
"Thought I'd mention this again in case it slipped through."
|
|
196
|
+
"Whenever convenient, the PR is still waiting."
|
|
197
|
+
"The PR continues to exist, should anyone be interested."
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Design philosophy
|
|
201
|
+
|
|
202
|
+
- Funny without being cruel
|
|
203
|
+
- Predictable and reproducible (via `seed`)
|
|
204
|
+
- Lightweight, pure Python, zero runtime dependencies
|
|
205
|
+
- Easy to extend with additional styles or phrases
|
|
206
|
+
- Beginner-friendly
|
|
207
|
+
|
|
208
|
+
## Contributing
|
|
209
|
+
|
|
210
|
+
New styles are the most fun thing to add. See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
211
|
+
for setup instructions and the checklist to run before opening a PR.
|
|
212
|
+
|
|
213
|
+
## Changelog
|
|
214
|
+
|
|
215
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT — see [LICENSE](LICENSE).
|
|
220
|
+
"# PassiveAggressive"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
passiveaggressive/__init__.py,sha256=RDMUVLCmjVDPpC0oHjHoKfocg_W8P5WG4S9olUKa7qg,492
|
|
2
|
+
passiveaggressive/cli.py,sha256=PquJAfoO0aHSZ6FhPvfP0H9wjCVoIuT50rYxcEL6MkU,1618
|
|
3
|
+
passiveaggressive/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
passiveaggressive/templates.py,sha256=0I8As-ET0ILZ0mvSsxRkFAAHvuElj8L8GUHqmMQBeUA,10362
|
|
5
|
+
passiveaggressive/translator.py,sha256=3Z8prI01LFy1FfFutd1YdvetEkrxKm_oo3_6pWFpHHw,4149
|
|
6
|
+
passiveaggressive/utils.py,sha256=_ti7y_rB5NbXjJ3wdgaqMIiKLdoBiFK9NNtuZNKmXY8,1848
|
|
7
|
+
passiveaggressive-0.1.0.dist-info/licenses/LICENSE,sha256=4odnr5Iqx6E26q6aefaTZWejNm6VsW8O6p3jpKseqwY,1087
|
|
8
|
+
passiveaggressive-0.1.0.dist-info/METADATA,sha256=52PySuh4ZBDXCjyjvV3AlaxGYKc9YGLgPXisdrooP00,7193
|
|
9
|
+
passiveaggressive-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
10
|
+
passiveaggressive-0.1.0.dist-info/entry_points.txt,sha256=zJRvRn2tQ6n-vh6kdnqj627tpfWZcgVswdKSdxReacY,65
|
|
11
|
+
passiveaggressive-0.1.0.dist-info/top_level.txt,sha256=S8mViVVDzqxXrdX8Ac5zwlAfcmaVF4R59q7_XqEC_YE,18
|
|
12
|
+
passiveaggressive-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 passiveaggressive contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
passiveaggressive
|