stage-cli 1.0.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.
Files changed (170) hide show
  1. stage/__init__.py +1 -0
  2. stage/__main__.py +8 -0
  3. stage/banner.py +32 -0
  4. stage/bootstrap/__init__.py +0 -0
  5. stage/bootstrap/openjobs.py +392 -0
  6. stage/classify/__init__.py +29 -0
  7. stage/classify/eligibility.py +115 -0
  8. stage/classify/internship.py +64 -0
  9. stage/classify/role.py +91 -0
  10. stage/classify/scope.py +47 -0
  11. stage/cli/__init__.py +0 -0
  12. stage/cli/app.py +4 -0
  13. stage/cli/commands/__init__.py +8 -0
  14. stage/cli/commands/discovery.py +294 -0
  15. stage/cli/commands/insight.py +494 -0
  16. stage/cli/commands/pipeline.py +337 -0
  17. stage/cli/commands/postings.py +473 -0
  18. stage/cli/commands/schedule.py +171 -0
  19. stage/cli/housekeeping.py +64 -0
  20. stage/cli/logfile.py +56 -0
  21. stage/cli/notify.py +170 -0
  22. stage/cli/options.py +678 -0
  23. stage/cli/render.py +1398 -0
  24. stage/cli/runlock.py +74 -0
  25. stage/cli/schedule.py +702 -0
  26. stage/cli/schedule_state.py +363 -0
  27. stage/cli/selection.py +83 -0
  28. stage/cli/serialize.py +196 -0
  29. stage/companies.py +542 -0
  30. stage/data/companies/a.yaml +1289 -0
  31. stage/data/companies/b.yaml +900 -0
  32. stage/data/companies/c.yaml +1377 -0
  33. stage/data/companies/d.yaml +497 -0
  34. stage/data/companies/e.yaml +519 -0
  35. stage/data/companies/f.yaml +454 -0
  36. stage/data/companies/g.yaml +601 -0
  37. stage/data/companies/h.yaml +446 -0
  38. stage/data/companies/i.yaml +503 -0
  39. stage/data/companies/j.yaml +138 -0
  40. stage/data/companies/k.yaml +278 -0
  41. stage/data/companies/l.yaml +402 -0
  42. stage/data/companies/m.yaml +937 -0
  43. stage/data/companies/n.yaml +549 -0
  44. stage/data/companies/o.yaml +371 -0
  45. stage/data/companies/other.yaml +58 -0
  46. stage/data/companies/p.yaml +825 -0
  47. stage/data/companies/q.yaml +121 -0
  48. stage/data/companies/r.yaml +583 -0
  49. stage/data/companies/s.yaml +1140 -0
  50. stage/data/companies/t.yaml +817 -0
  51. stage/data/companies/u.yaml +196 -0
  52. stage/data/companies/v.yaml +325 -0
  53. stage/data/companies/w.yaml +353 -0
  54. stage/data/companies/x.yaml +67 -0
  55. stage/data/companies/y.yaml +36 -0
  56. stage/data/companies/z.yaml +146 -0
  57. stage/data/fonts/DejaVuSans.LICENSE.txt +99 -0
  58. stage/data/fonts/DejaVuSans.ttf +0 -0
  59. stage/data/lexicon/company_tokens.yaml +228 -0
  60. stage/data/lexicon/eligibility.yaml +455 -0
  61. stage/data/lexicon/inclusive_suffixes.yaml +37 -0
  62. stage/data/lexicon/internship.yaml +187 -0
  63. stage/data/lexicon/language.yaml +226 -0
  64. stage/data/lexicon/locations.yaml +1159 -0
  65. stage/data/lexicon/roles.yaml +2012 -0
  66. stage/data/lexicon/terms.yaml +76 -0
  67. stage/data/lexicon/workday_facets.yaml +27 -0
  68. stage/data/seed_companies.yaml +198 -0
  69. stage/dedup/__init__.py +19 -0
  70. stage/dedup/identity.py +113 -0
  71. stage/dedup/resolve.py +97 -0
  72. stage/domain/__init__.py +244 -0
  73. stage/domain/company.py +49 -0
  74. stage/domain/coverage.py +86 -0
  75. stage/domain/custom_board.py +92 -0
  76. stage/domain/discovery.py +94 -0
  77. stage/domain/enums.py +114 -0
  78. stage/domain/events.py +204 -0
  79. stage/domain/filters.py +27 -0
  80. stage/domain/health.py +169 -0
  81. stage/domain/ids.py +48 -0
  82. stage/domain/job.py +47 -0
  83. stage/domain/matching.py +15 -0
  84. stage/domain/priority.py +34 -0
  85. stage/domain/quarantine.py +39 -0
  86. stage/domain/rate_state.py +78 -0
  87. stage/domain/retention.py +20 -0
  88. stage/domain/rotation.py +46 -0
  89. stage/domain/signals.py +12 -0
  90. stage/domain/sync_run.py +35 -0
  91. stage/domain/text.py +113 -0
  92. stage/domain/validator.py +14 -0
  93. stage/domain/visits.py +60 -0
  94. stage/domain/workday.py +38 -0
  95. stage/http/__init__.py +58 -0
  96. stage/http/breaker.py +53 -0
  97. stage/http/cache.py +44 -0
  98. stage/http/client.py +725 -0
  99. stage/http/profiles.py +101 -0
  100. stage/lexicon.py +370 -0
  101. stage/normalize/__init__.py +16 -0
  102. stage/normalize/language.py +47 -0
  103. stage/normalize/location.py +271 -0
  104. stage/normalize/terms.py +153 -0
  105. stage/normalize/urls.py +122 -0
  106. stage/paths.py +86 -0
  107. stage/py.typed +0 -0
  108. stage/services/__init__.py +0 -0
  109. stage/services/canary.py +120 -0
  110. stage/services/coverage.py +231 -0
  111. stage/services/discover.py +747 -0
  112. stage/services/export.py +274 -0
  113. stage/services/health.py +237 -0
  114. stage/services/maintenance.py +225 -0
  115. stage/services/quarantine.py +20 -0
  116. stage/services/query.py +86 -0
  117. stage/services/sync.py +1257 -0
  118. stage/sources/__init__.py +82 -0
  119. stage/sources/_text.py +79 -0
  120. stage/sources/ashby.py +93 -0
  121. stage/sources/bamboohr.py +80 -0
  122. stage/sources/base.py +225 -0
  123. stage/sources/breezy.py +90 -0
  124. stage/sources/collage.py +60 -0
  125. stage/sources/community_feeds.py +142 -0
  126. stage/sources/curated_markdown.py +289 -0
  127. stage/sources/custom_json.py +610 -0
  128. stage/sources/espresso.py +154 -0
  129. stage/sources/feed.py +44 -0
  130. stage/sources/greenhouse.py +104 -0
  131. stage/sources/jobbank.py +147 -0
  132. stage/sources/jobvite.py +133 -0
  133. stage/sources/lever.py +76 -0
  134. stage/sources/oracle_cloud.py +187 -0
  135. stage/sources/platforms.py +609 -0
  136. stage/sources/quebec_emploi.py +146 -0
  137. stage/sources/recruitee.py +96 -0
  138. stage/sources/simplify.py +110 -0
  139. stage/sources/smartrecruiters.py +216 -0
  140. stage/sources/speedyapply.py +200 -0
  141. stage/sources/themuse.py +157 -0
  142. stage/sources/workable.py +83 -0
  143. stage/sources/workday.py +524 -0
  144. stage/sources/zshah.py +99 -0
  145. stage/storage/__init__.py +29 -0
  146. stage/storage/migrations/0001_initial.sql +239 -0
  147. stage/storage/migrations/__init__.py +135 -0
  148. stage/storage/repository.py +213 -0
  149. stage/storage/search.py +28 -0
  150. stage/storage/sqlite_repo.py +1586 -0
  151. stage/storage/writer.py +249 -0
  152. stage/tui/__init__.py +0 -0
  153. stage/tui/app.py +82 -0
  154. stage/tui/help.py +26 -0
  155. stage/tui/safe.py +21 -0
  156. stage/tui/screens/__init__.py +0 -0
  157. stage/tui/screens/boards.py +186 -0
  158. stage/tui/screens/postings.py +509 -0
  159. stage/tui/screens/review.py +209 -0
  160. stage/tui/screens/splash.py +37 -0
  161. stage/tui/screens/stats.py +124 -0
  162. stage/tui/screens/sync.py +194 -0
  163. stage/tui/state.py +160 -0
  164. stage/tui/theme.tcss +205 -0
  165. stage/tui/widgets/__init__.py +0 -0
  166. stage_cli-1.0.0.dist-info/METADATA +379 -0
  167. stage_cli-1.0.0.dist-info/RECORD +170 -0
  168. stage_cli-1.0.0.dist-info/WHEEL +4 -0
  169. stage_cli-1.0.0.dist-info/entry_points.txt +2 -0
  170. stage_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
stage/tui/state.py ADDED
@@ -0,0 +1,160 @@
1
+ from dataclasses import dataclass, field, replace
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from stage.domain import (
6
+ DEFAULT_WINDOW_DAYS,
7
+ JobFilters,
8
+ Language,
9
+ LocationBucket,
10
+ RoleCategory,
11
+ )
12
+
13
+ DEBOUNCE_SECONDS = 0.04
14
+ PAGE_SIZE = 200
15
+
16
+ LAST_DAYS_CHOICES: tuple[int, ...] = (7, 14, 30, 0)
17
+
18
+ FILTER_FIELDS: tuple[tuple[str, str, tuple[str, ...]], ...] = (
19
+ ("role", "Role", tuple(value.value for value in RoleCategory if value.value != "hardware")),
20
+ ("location", "Location", tuple(value.value for value in LocationBucket)),
21
+ ("language", "Language", tuple(value.value for value in Language)),
22
+ )
23
+
24
+ _ENUMS: dict[str, Any] = {
25
+ "role": RoleCategory,
26
+ "location": LocationBucket,
27
+ "language": Language,
28
+ }
29
+
30
+ _OPEN_FIELDS: tuple[tuple[str, str], ...] = (("company", "Company"),)
31
+
32
+
33
+ def filter_rows(
34
+ state: "FilterState", employer: str | None = None
35
+ ) -> tuple[tuple[str, str, str, bool], ...]:
36
+ rows: list[tuple[str, str, str, bool]] = []
37
+ for name, label, options in FILTER_FIELDS:
38
+ for option in options:
39
+ if option == "unknown":
40
+ continue
41
+ rows.append((name, option, f"{label}: {option}", state.values.get(name) == option))
42
+ window = state.last_days
43
+ for days in LAST_DAYS_CHOICES:
44
+ shown = "any date" if days == 0 else f"last {days} days"
45
+ rows.append(("last_days", str(days), f"Window: {shown}", window == days))
46
+ rows.append(("only_new", "on", "Only what the last sync brought in", state.only_new))
47
+ rows.append(("show_all", "on", "Show every match, not just a page", state.show_all))
48
+ chosen = state.values.get("company")
49
+ if chosen is not None:
50
+ rows.append(("company", chosen, f"Employer: {chosen}", True))
51
+ elif employer is not None:
52
+ rows.append(("company", employer, f"Employer: {employer}", False))
53
+ return tuple(rows)
54
+
55
+
56
+ @dataclass
57
+ class FilterState:
58
+ query: str = ""
59
+ limit: int = PAGE_SIZE
60
+ values: dict[str, str] = field(default_factory=dict)
61
+ last_days: int = DEFAULT_WINDOW_DAYS
62
+ only_new: bool = False
63
+ show_all: bool = False
64
+
65
+ def toggle(self, name: str, value: str) -> None:
66
+ if self.values.get(name) == value:
67
+ self.values.pop(name, None)
68
+ return
69
+ self.values[name] = value
70
+
71
+ def clear(self) -> None:
72
+ self.values.clear()
73
+ self.query = ""
74
+ self.limit = PAGE_SIZE
75
+ self.last_days = DEFAULT_WINDOW_DAYS
76
+ self.only_new = False
77
+ self.show_all = False
78
+
79
+ def widen(self) -> None:
80
+ self.limit += PAGE_SIZE
81
+
82
+ def narrow(self) -> bool:
83
+ if self.limit <= PAGE_SIZE:
84
+ return False
85
+ self.limit = max(PAGE_SIZE, self.limit - PAGE_SIZE)
86
+ return True
87
+
88
+ def choose(self, name: str, value: str) -> None:
89
+ if name == "last_days":
90
+ self.last_days = int(value)
91
+ elif name == "only_new":
92
+ self.only_new = not self.only_new
93
+ elif name == "show_all":
94
+ self.show_all = not self.show_all
95
+ else:
96
+ self.toggle(name, value)
97
+ self.limit = PAGE_SIZE
98
+
99
+ @property
100
+ def window_days(self) -> int | None:
101
+ return self.last_days or None
102
+
103
+ @property
104
+ def active(self) -> tuple[tuple[str, str], ...]:
105
+ named = tuple(name for name, _, _ in FILTER_FIELDS)
106
+ ordered = (*named, *(name for name, _ in _OPEN_FIELDS))
107
+ return tuple((name, self.values[name]) for name in ordered if name in self.values)
108
+
109
+ def as_filters(self) -> JobFilters:
110
+ chosen: dict[str, Any] = {}
111
+ for name, value in self.values.items():
112
+ enum = _ENUMS.get(name)
113
+ if enum is None:
114
+ if any(name == field_name for field_name, _ in _OPEN_FIELDS):
115
+ chosen[name] = value
116
+ continue
117
+ try:
118
+ chosen[name] = enum(value)
119
+ except ValueError:
120
+ continue
121
+ return JobFilters(limit=None if self.show_all else self.limit, **chosen)
122
+
123
+
124
+ def theme_path() -> Path:
125
+ from stage.paths import data_dir
126
+
127
+ return data_dir() / "tui-theme"
128
+
129
+
130
+ def load_theme(path: Path | None = None) -> str | None:
131
+ target = path or theme_path()
132
+ try:
133
+ name = target.read_text(encoding="utf-8").strip()
134
+ except OSError:
135
+ return None
136
+ return name or None
137
+
138
+
139
+ def store_theme(name: str, path: Path | None = None) -> bool:
140
+ target = path or theme_path()
141
+ try:
142
+ target.parent.mkdir(parents=True, exist_ok=True)
143
+ target.write_text(name, encoding="utf-8")
144
+ except OSError:
145
+ return False
146
+ return True
147
+
148
+
149
+ __all__ = [
150
+ "DEBOUNCE_SECONDS",
151
+ "FILTER_FIELDS",
152
+ "LAST_DAYS_CHOICES",
153
+ "PAGE_SIZE",
154
+ "FilterState",
155
+ "filter_rows",
156
+ "load_theme",
157
+ "replace",
158
+ "store_theme",
159
+ "theme_path",
160
+ ]
stage/tui/theme.tcss ADDED
@@ -0,0 +1,205 @@
1
+ $blue: $primary;
2
+ $on-blue: $text-primary;
3
+ $blue-deep: $primary-muted;
4
+ $blue-dim: $primary-darken-2;
5
+ $new: $success;
6
+ $warn: $warning;
7
+ $bad: $error;
8
+
9
+ Screen {
10
+ background: $surface;
11
+ }
12
+
13
+ Header {
14
+ background: $blue-deep;
15
+ color: $on-blue;
16
+ }
17
+
18
+ #splash {
19
+ align: center middle;
20
+ background: $surface;
21
+ }
22
+
23
+ #splash-art {
24
+ color: $text;
25
+ text-style: bold;
26
+ text-align: center;
27
+ width: auto;
28
+ }
29
+
30
+ #splash-stats {
31
+ color: $text-muted;
32
+ text-align: center;
33
+ margin-top: 1;
34
+ width: auto;
35
+ }
36
+
37
+ #chips {
38
+ height: auto;
39
+ min-height: 1;
40
+ padding: 0 1;
41
+ background: $panel;
42
+ color: $text-muted;
43
+ border-bottom: solid $blue-dim;
44
+ }
45
+
46
+ #chips .active {
47
+ color: $blue;
48
+ text-style: bold;
49
+ }
50
+
51
+ #chips.busy {
52
+ color: $blue;
53
+ }
54
+
55
+ #overall {
56
+ margin: 0 1;
57
+ }
58
+
59
+ #sources {
60
+ padding: 1;
61
+ }
62
+
63
+ #warnings {
64
+ padding: 0 1;
65
+ color: $warn;
66
+ }
67
+
68
+ #filters {
69
+ height: auto;
70
+ max-height: 12;
71
+ padding: 0 1;
72
+ background: $panel;
73
+ border-bottom: solid $blue;
74
+ scrollbar-size-vertical: 1;
75
+ }
76
+
77
+ #filters:focus {
78
+ border-bottom: solid $blue;
79
+ }
80
+
81
+ #filters > .option-list--option-highlighted {
82
+ background: $blue-deep;
83
+ color: $on-blue;
84
+ text-style: bold;
85
+ }
86
+
87
+ #filters.hidden {
88
+ display: none;
89
+ }
90
+
91
+ #results {
92
+ height: 1fr;
93
+ min-height: 3;
94
+ scrollbar-size-vertical: 1;
95
+ }
96
+
97
+ #results > .datatable--header {
98
+ background: $panel;
99
+ color: $blue;
100
+ text-style: bold;
101
+ }
102
+
103
+ #results > .datatable--cursor {
104
+ background: $blue-deep;
105
+ color: $on-blue;
106
+ text-style: bold;
107
+ }
108
+
109
+ #results > .datatable--hover {
110
+ background: $blue-dim;
111
+ }
112
+
113
+ DataTable {
114
+ height: 1fr;
115
+ }
116
+
117
+ DataTable > .datatable--cursor {
118
+ background: $blue-deep;
119
+ color: $on-blue;
120
+ }
121
+
122
+ DataTable > .datatable--header {
123
+ background: $panel;
124
+ color: $blue;
125
+ text-style: bold;
126
+ }
127
+
128
+ #detail {
129
+ height: 9;
130
+ min-height: 3;
131
+ padding: 1 2;
132
+ background: $panel;
133
+ border-top: solid $blue;
134
+ }
135
+
136
+ #detail.expanded {
137
+ height: 22;
138
+ }
139
+
140
+ #detail-title {
141
+ color: $blue;
142
+ text-style: bold;
143
+ }
144
+
145
+ #search-bar {
146
+ height: 3;
147
+ padding: 0 1;
148
+ display: none;
149
+ }
150
+
151
+ #search-bar.visible {
152
+ display: block;
153
+ }
154
+
155
+ Input {
156
+ border: tall $blue-dim;
157
+ }
158
+
159
+ Input:focus {
160
+ border: tall $blue;
161
+ }
162
+
163
+ .empty {
164
+ color: $text-muted;
165
+ text-align: center;
166
+ padding: 2;
167
+ }
168
+
169
+ #stats-grid {
170
+ height: 1fr;
171
+ padding: 1;
172
+ }
173
+
174
+ .bar {
175
+ color: $blue;
176
+ }
177
+
178
+ .stat-label {
179
+ color: $text-muted;
180
+ }
181
+
182
+ Footer {
183
+ background: $panel;
184
+ }
185
+
186
+ .narrow #filters {
187
+ max-height: 8;
188
+ }
189
+
190
+ #help {
191
+ display: none;
192
+ }
193
+
194
+ #help.visible {
195
+ display: block;
196
+ layer: overlay;
197
+ dock: top;
198
+ width: 46;
199
+ height: auto;
200
+ margin: 2 0 0 4;
201
+ padding: 1 2;
202
+ background: $panel;
203
+ border: round $blue;
204
+ color: $text;
205
+ }
File without changes