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.
@@ -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"
@@ -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
+ [![CI](https://github.com/example/passiveaggressive/actions/workflows/ci.yml/badge.svg)](https://github.com/example/passiveaggressive/actions/workflows/ci.yml)
36
+ [![PyPI](https://img.shields.io/pypi/v/passiveaggressive.svg)](https://pypi.org/project/passiveaggressive/)
37
+ [![Python versions](https://img.shields.io/pypi/pyversions/passiveaggressive.svg)](https://pypi.org/project/passiveaggressive/)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ passiveaggressive = passiveaggressive.cli:main
@@ -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