applyr 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.
- applyr/__init__.py +3 -0
- applyr/cli.py +245 -0
- applyr/commands.py +971 -0
- applyr/config.py +131 -0
- applyr/cv.py +113 -0
- applyr/db.py +116 -0
- applyr/scoring.py +36 -0
- applyr-0.1.0.dist-info/METADATA +234 -0
- applyr-0.1.0.dist-info/RECORD +13 -0
- applyr-0.1.0.dist-info/WHEEL +5 -0
- applyr-0.1.0.dist-info/entry_points.txt +2 -0
- applyr-0.1.0.dist-info/licenses/LICENSE +21 -0
- applyr-0.1.0.dist-info/top_level.txt +1 -0
applyr/__init__.py
ADDED
applyr/cli.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""CLI entry point — argparse setup, command routing, error handling."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from applyr import __version__
|
|
6
|
+
from applyr.commands import (
|
|
7
|
+
cmd_add,
|
|
8
|
+
cmd_delete,
|
|
9
|
+
cmd_export,
|
|
10
|
+
cmd_followups,
|
|
11
|
+
cmd_gaps,
|
|
12
|
+
cmd_init,
|
|
13
|
+
cmd_list,
|
|
14
|
+
cmd_pipeline,
|
|
15
|
+
cmd_search,
|
|
16
|
+
cmd_show,
|
|
17
|
+
cmd_stats,
|
|
18
|
+
cmd_summary,
|
|
19
|
+
cmd_trends,
|
|
20
|
+
cmd_update,
|
|
21
|
+
)
|
|
22
|
+
from applyr.cv import cmd_cv_generate, cmd_cv_pdf
|
|
23
|
+
from applyr.db import init_db, VALID_STATUSES
|
|
24
|
+
|
|
25
|
+
USAGE = f"""\
|
|
26
|
+
applyr v{__version__} — CLI job application tracker for AI coding agents
|
|
27
|
+
|
|
28
|
+
Usage: applyr <command> [options]
|
|
29
|
+
|
|
30
|
+
Commands:
|
|
31
|
+
init Set up ~/.applyr/ (config, database, templates)
|
|
32
|
+
add '<json>' Register a new job offer
|
|
33
|
+
list [--status S] [--sort F] List offers (default: last 50)
|
|
34
|
+
pipeline [--min-score N] View offers grouped by status
|
|
35
|
+
show <id> Show full offer details
|
|
36
|
+
update <id> <status> [opts] Update offer status
|
|
37
|
+
delete <id> Delete an offer
|
|
38
|
+
search <keyword> [--status S] Search by company/title/notes/tech_stack
|
|
39
|
+
stats Conversion funnel and metrics
|
|
40
|
+
gaps [--limit N] Skill gap analysis
|
|
41
|
+
followups Pending/overdue follow-ups
|
|
42
|
+
trends [--period week|month] Application trends over time
|
|
43
|
+
summary [--json] Weekly summary (LLM-optimized)
|
|
44
|
+
export [--format csv|json] Export all data
|
|
45
|
+
version Show version
|
|
46
|
+
help Show this help
|
|
47
|
+
|
|
48
|
+
Statuses: {' | '.join(VALID_STATUSES)}
|
|
49
|
+
|
|
50
|
+
Docs: https://github.com/DeibyGS/applyr
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _safe_int(value: str, label: str = "ID") -> int | None:
|
|
55
|
+
"""Parse string to int, print error and return None on failure."""
|
|
56
|
+
try:
|
|
57
|
+
return int(value)
|
|
58
|
+
except ValueError:
|
|
59
|
+
print(f"Error: {label} must be an integer, got: {value}")
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_flag(args: list[str], flag: str) -> str | None:
|
|
64
|
+
"""Get the value after a flag like --status, or None if not present."""
|
|
65
|
+
if flag not in args:
|
|
66
|
+
return None
|
|
67
|
+
idx = args.index(flag)
|
|
68
|
+
if idx + 1 >= len(args):
|
|
69
|
+
print(f"Error: {flag} requires a value")
|
|
70
|
+
return None
|
|
71
|
+
return args[idx + 1]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _has_flag(args: list[str], flag: str) -> bool:
|
|
75
|
+
"""Check if a flag is present."""
|
|
76
|
+
return flag in args
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main():
|
|
80
|
+
args = sys.argv[1:]
|
|
81
|
+
|
|
82
|
+
if not args or args[0] in ("help", "--help", "-h"):
|
|
83
|
+
print(USAGE)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
cmd = args[0]
|
|
87
|
+
|
|
88
|
+
if cmd in ("version", "--version", "-v"):
|
|
89
|
+
print(f"applyr v{__version__}")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Commands that need DB initialized
|
|
93
|
+
if cmd != "init":
|
|
94
|
+
init_db()
|
|
95
|
+
|
|
96
|
+
if cmd == "init":
|
|
97
|
+
cmd_init()
|
|
98
|
+
|
|
99
|
+
elif cmd == "add":
|
|
100
|
+
if len(args) < 2:
|
|
101
|
+
print("Usage: applyr add '<json>'")
|
|
102
|
+
print(" Required: title")
|
|
103
|
+
print(" Optional: company, summary, date_received, date_applied,")
|
|
104
|
+
print(" compatibility_pct, status, canal, work_mode,")
|
|
105
|
+
print(" location, salary_min, salary_max, seniority_level,")
|
|
106
|
+
print(" role_category, tech_stack, cover_letter, job_url,")
|
|
107
|
+
print(" contact_name, contact_role, topics, notes")
|
|
108
|
+
return
|
|
109
|
+
cmd_add(args[1])
|
|
110
|
+
|
|
111
|
+
elif cmd == "list":
|
|
112
|
+
status = _get_flag(args, "--status")
|
|
113
|
+
sort_by = _get_flag(args, "--sort") or "date_applied"
|
|
114
|
+
limit = None
|
|
115
|
+
if _has_flag(args, "--all"):
|
|
116
|
+
limit = -1
|
|
117
|
+
elif _get_flag(args, "--limit") is not None:
|
|
118
|
+
limit = _safe_int(_get_flag(args, "--limit"), "--limit")
|
|
119
|
+
if limit is None:
|
|
120
|
+
return
|
|
121
|
+
cmd_list(status_filter=status, sort_by=sort_by, limit=limit)
|
|
122
|
+
|
|
123
|
+
elif cmd == "pipeline":
|
|
124
|
+
min_score = 0
|
|
125
|
+
raw = _get_flag(args, "--min-score")
|
|
126
|
+
if raw is not None:
|
|
127
|
+
min_score = _safe_int(raw, "--min-score")
|
|
128
|
+
if min_score is None:
|
|
129
|
+
return
|
|
130
|
+
cmd_pipeline(min_score=min_score)
|
|
131
|
+
|
|
132
|
+
elif cmd == "show":
|
|
133
|
+
if len(args) < 2:
|
|
134
|
+
print("Usage: applyr show <id>")
|
|
135
|
+
return
|
|
136
|
+
offer_id = _safe_int(args[1])
|
|
137
|
+
if offer_id is not None:
|
|
138
|
+
cmd_show(offer_id)
|
|
139
|
+
|
|
140
|
+
elif cmd == "update":
|
|
141
|
+
if len(args) < 3:
|
|
142
|
+
print("Usage: applyr update <id> <status> [--notes '...'] [--canal '...']")
|
|
143
|
+
print(f" Statuses: {', '.join(VALID_STATUSES)}")
|
|
144
|
+
return
|
|
145
|
+
offer_id = _safe_int(args[1])
|
|
146
|
+
if offer_id is None:
|
|
147
|
+
return
|
|
148
|
+
status = args[2]
|
|
149
|
+
notes = _get_flag(args, "--notes")
|
|
150
|
+
canal = _get_flag(args, "--canal")
|
|
151
|
+
cmd_update(offer_id, status, notes, canal)
|
|
152
|
+
|
|
153
|
+
elif cmd == "delete":
|
|
154
|
+
if len(args) < 2:
|
|
155
|
+
print("Usage: applyr delete <id>")
|
|
156
|
+
return
|
|
157
|
+
offer_id = _safe_int(args[1])
|
|
158
|
+
if offer_id is not None:
|
|
159
|
+
cmd_delete(offer_id)
|
|
160
|
+
|
|
161
|
+
elif cmd == "search":
|
|
162
|
+
if len(args) < 2:
|
|
163
|
+
print("Usage: applyr search <keyword> [--status S]")
|
|
164
|
+
return
|
|
165
|
+
status = _get_flag(args, "--status")
|
|
166
|
+
# Collect keyword (everything between cmd and flags)
|
|
167
|
+
keyword_parts = []
|
|
168
|
+
i = 1
|
|
169
|
+
while i < len(args):
|
|
170
|
+
if args[i].startswith("--"):
|
|
171
|
+
i += 2 # skip flag and its value
|
|
172
|
+
continue
|
|
173
|
+
keyword_parts.append(args[i])
|
|
174
|
+
i += 1
|
|
175
|
+
keyword = " ".join(keyword_parts)
|
|
176
|
+
cmd_search(keyword, status_filter=status)
|
|
177
|
+
|
|
178
|
+
elif cmd == "stats":
|
|
179
|
+
cmd_stats()
|
|
180
|
+
|
|
181
|
+
elif cmd == "gaps":
|
|
182
|
+
limit = 10
|
|
183
|
+
raw = _get_flag(args, "--limit")
|
|
184
|
+
if raw is not None:
|
|
185
|
+
limit = _safe_int(raw, "--limit")
|
|
186
|
+
if limit is None:
|
|
187
|
+
return
|
|
188
|
+
cmd_gaps(limit=limit)
|
|
189
|
+
|
|
190
|
+
elif cmd == "followups":
|
|
191
|
+
cmd_followups()
|
|
192
|
+
|
|
193
|
+
elif cmd == "trends":
|
|
194
|
+
period = _get_flag(args, "--period") or "week"
|
|
195
|
+
if period not in ("week", "month"):
|
|
196
|
+
print(f"Error: period must be 'week' or 'month', got: {period}")
|
|
197
|
+
return
|
|
198
|
+
cmd_trends(period=period)
|
|
199
|
+
|
|
200
|
+
elif cmd == "summary":
|
|
201
|
+
as_json = _has_flag(args, "--json")
|
|
202
|
+
cmd_summary(as_json=as_json)
|
|
203
|
+
|
|
204
|
+
elif cmd == "export":
|
|
205
|
+
fmt = _get_flag(args, "--format") or "csv"
|
|
206
|
+
if fmt not in ("csv", "json"):
|
|
207
|
+
print(f"Error: format must be 'csv' or 'json', got: {fmt}")
|
|
208
|
+
return
|
|
209
|
+
filepath = _get_flag(args, "--file")
|
|
210
|
+
cmd_export(fmt=fmt, filepath=filepath)
|
|
211
|
+
|
|
212
|
+
elif cmd == "cv":
|
|
213
|
+
if len(args) < 2:
|
|
214
|
+
print("Usage:")
|
|
215
|
+
print(" applyr cv generate <id> [--template ats] Generate CV for offer")
|
|
216
|
+
print(" applyr cv pdf <html-file> [--output f.pdf] HTML to PDF via Chrome")
|
|
217
|
+
return
|
|
218
|
+
subcmd = args[1]
|
|
219
|
+
if subcmd == "generate":
|
|
220
|
+
if len(args) < 3:
|
|
221
|
+
print("Usage: applyr cv generate <id> [--template ats]")
|
|
222
|
+
return
|
|
223
|
+
offer_id = _safe_int(args[2])
|
|
224
|
+
if offer_id is None:
|
|
225
|
+
return
|
|
226
|
+
template = _get_flag(args, "--template") or "ats"
|
|
227
|
+
cmd_cv_generate(offer_id, template=template)
|
|
228
|
+
elif subcmd == "pdf":
|
|
229
|
+
if len(args) < 3:
|
|
230
|
+
print("Usage: applyr cv pdf <html-file> [--output file.pdf]")
|
|
231
|
+
return
|
|
232
|
+
html_file = args[2]
|
|
233
|
+
output = _get_flag(args, "--output")
|
|
234
|
+
cmd_cv_pdf(html_file, output=output)
|
|
235
|
+
else:
|
|
236
|
+
print(f"Unknown cv subcommand: '{subcmd}'")
|
|
237
|
+
print(" Available: generate, pdf")
|
|
238
|
+
|
|
239
|
+
else:
|
|
240
|
+
print(f"Unknown command: '{cmd}'")
|
|
241
|
+
print(f"Run 'applyr help' to see available commands.")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
main()
|