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
@@ -0,0 +1,76 @@
1
+ summer:
2
+ - summer
3
+ - été
4
+ - ete
5
+ - estival
6
+ - estivale
7
+ - summer term
8
+ - session d'été
9
+ - stage d'été
10
+
11
+ fall:
12
+ - fall
13
+ - autumn
14
+ - automne
15
+ - fall term
16
+ - session d'automne
17
+ - stage d'automne
18
+
19
+ winter:
20
+ - winter
21
+ - hiver
22
+ - hivernale
23
+ - winter term
24
+ - session d'hiver
25
+ - stage d'hiver
26
+
27
+ spring:
28
+ - spring
29
+ - printemps
30
+ - spring term
31
+ - session du printemps
32
+ - stage du printemps
33
+
34
+ fillers:
35
+ - of
36
+ - de
37
+ - du
38
+ - des
39
+ - d
40
+ - the
41
+ - la
42
+ - le
43
+ - term
44
+ - session
45
+ - start
46
+ - starting
47
+ - intake
48
+ - cohort
49
+ - programme
50
+ - program
51
+
52
+ blocked_bigrams:
53
+ - spring boot
54
+ - spring framework
55
+ - spring frameworks
56
+ - spring security
57
+ - spring mvc
58
+ - spring data
59
+ - spring cloud
60
+ - spring batch
61
+ - spring actuator
62
+ - spring health
63
+ - spring environments
64
+ - spring integration
65
+ - spring webflux
66
+ - summer camp
67
+ - winter garden
68
+ - fall protection
69
+ - fall risk
70
+ - fall arrest
71
+ - spring loaded
72
+ - spring valley
73
+ - winter park
74
+ - summer street
75
+ - spring street
76
+ - spring hill
@@ -0,0 +1,27 @@
1
+ facet_parameters:
2
+ - timeType
3
+ - workerSubType
4
+ - jobFamily
5
+ - employmentType
6
+
7
+ intern_descriptors:
8
+ - intern
9
+ - interns
10
+ - internship
11
+ - internships
12
+ - intern/co-op
13
+ - co-op
14
+ - coop
15
+ - co-op/intern
16
+ - co-op student
17
+ - stage
18
+ - stagiaire
19
+ - stage etudiant
20
+ - stagiaire etudiant
21
+ - student
22
+ - student worker
23
+ - etudiant
24
+ - alternance
25
+ - alternant
26
+ - summer intern
27
+ - summer student
@@ -0,0 +1,198 @@
1
+ - { name: Behaviour Interactive, section: montreal, priority: high }
2
+ - { name: Bombardier, section: montreal, priority: high }
3
+ - { name: CAE, section: montreal, priority: high }
4
+ - { name: Coveo, section: montreal, priority: high }
5
+ - { name: Desjardins, section: montreal, priority: high }
6
+ - { name: Eidos-Montréal, section: montreal, priority: high }
7
+ - { name: Gameloft, section: montreal, priority: high }
8
+ - { name: Genetec, section: montreal, priority: high }
9
+ - { name: Hydro-Québec, section: montreal, priority: high }
10
+ - { name: Intact, section: montreal, priority: high }
11
+ - { name: Laurentian Bank, section: montreal, priority: high }
12
+ - { name: Lightspeed, section: montreal, priority: high }
13
+ - { name: Matrox, section: montreal, priority: high }
14
+ - { name: Mila, section: montreal, priority: high }
15
+ - { name: National Bank of Canada, section: montreal, priority: high }
16
+ - { name: Nuvei, section: montreal, priority: high, note: not listed, consider }
17
+ - { name: Pratt & Whitney Canada, section: montreal, priority: high }
18
+ - { name: Squarepoint Capital, section: montreal, priority: high }
19
+ - { name: Ubisoft, section: montreal, priority: high }
20
+ - { name: Valsoft, section: montreal, priority: high }
21
+ - { name: Videotron, section: montreal, priority: high }
22
+ - { name: Warner Bros Games, section: montreal, priority: high }
23
+
24
+ - { name: BlackBerry, section: canada, priority: high }
25
+ - { name: BlackBerry QNX, section: canada, priority: high }
26
+ - { name: BMO, section: canada, priority: high }
27
+ - { name: Bell, section: canada, priority: high }
28
+ - { name: CGI, section: canada, priority: high }
29
+ - { name: CIBC, section: canada, priority: high }
30
+ - { name: Ciena, section: canada, priority: high }
31
+ - { name: Interac, section: canada, priority: high }
32
+ - { name: Mackenzie Investments, section: canada, priority: high }
33
+ - { name: Manulife, section: canada, priority: high }
34
+ - { name: MDA Space, section: canada, priority: high }
35
+ - { name: OpenText, section: canada, priority: high, note: not listed, consider }
36
+ - { name: RBC, section: canada, priority: high }
37
+ - { name: Rogers, section: canada, priority: high }
38
+ - { name: Scotiabank, section: canada, priority: high }
39
+ - { name: Shopify, section: canada, priority: high }
40
+ - { name: Sun Life, section: canada, priority: high }
41
+ - { name: TD Bank, section: canada, priority: high }
42
+ - { name: Telus, section: canada, priority: high }
43
+ - { name: Wealthsimple, section: canada, priority: high }
44
+
45
+ - { name: Akuna Capital, section: quant, priority: normal }
46
+ - { name: Citadel, section: quant, priority: normal }
47
+ - { name: Citadel Securities, section: quant, priority: normal }
48
+ - { name: D.E. Shaw, section: quant, priority: normal }
49
+ - { name: DRW, section: quant, priority: normal }
50
+ - { name: Hudson River Trading, section: quant, priority: normal }
51
+ - { name: Jane Street, section: quant, priority: normal }
52
+ - { name: Jump Trading, section: quant, priority: normal }
53
+ - { name: Maven Securities, section: quant, priority: normal }
54
+ - { name: Optiver, section: quant, priority: normal }
55
+ - { name: Renaissance Technologies, section: quant, priority: low, note: does not recruit publicly }
56
+ - { name: Tower Research Capital, section: quant, priority: normal }
57
+ - { name: Two Sigma, section: quant, priority: normal }
58
+
59
+ - { name: Adobe, section: bigtech, priority: normal }
60
+ - { name: Amazon, section: bigtech, priority: normal }
61
+ - { name: AMD, section: bigtech, priority: normal }
62
+ - { name: Apple, section: bigtech, priority: normal }
63
+ - { name: Broadcom, section: bigtech, priority: normal }
64
+ - { name: ByteDance, section: bigtech, priority: normal }
65
+ - { name: Cisco, section: bigtech, priority: normal }
66
+ - { name: Cisco Meraki, section: bigtech, priority: normal }
67
+ - { name: Cisco ThousandEyes, section: bigtech, priority: normal }
68
+ - { name: Dell, section: bigtech, priority: normal }
69
+ - { name: Ericsson, section: bigtech, priority: normal }
70
+ - { name: Google, section: bigtech, priority: normal }
71
+ - { name: Hewlett Packard Enterprise, section: bigtech, priority: normal }
72
+ - { name: Hitachi, section: bigtech, priority: low, note: low yield for a Montreal student }
73
+ - { name: Huawei, section: bigtech, priority: normal }
74
+ - { name: IBM, section: bigtech, priority: normal }
75
+ - { name: Intel, section: bigtech, priority: normal }
76
+ - { name: LinkedIn, section: bigtech, priority: normal }
77
+ - { name: Meta, section: bigtech, priority: normal }
78
+ - { name: Microsoft, section: bigtech, priority: normal }
79
+ - { name: Motorola, section: bigtech, priority: low, note: low yield for a Montreal student }
80
+ - { name: Netflix, section: bigtech, priority: normal }
81
+ - { name: Nokia, section: bigtech, priority: normal }
82
+ - { name: NVIDIA, section: bigtech, priority: normal }
83
+ - { name: Oracle, section: bigtech, priority: normal }
84
+ - { name: Qualcomm, section: bigtech, priority: normal, note: not listed }
85
+ - { name: Salesforce, section: bigtech, priority: normal }
86
+ - { name: Samsung, section: bigtech, priority: normal }
87
+ - { name: SAP, section: bigtech, priority: normal }
88
+ - { name: Slack, section: bigtech, priority: normal }
89
+ - { name: Sony Interactive Entertainment, section: bigtech, priority: normal }
90
+ - { name: TikTok, section: bigtech, priority: normal }
91
+ - { name: Tesla, section: bigtech, priority: normal }
92
+ - { name: Uber, section: bigtech, priority: normal }
93
+ - { name: VMware, section: bigtech, priority: normal }
94
+ - { name: X (Twitter), section: bigtech, priority: normal }
95
+ - { name: Zoom, section: bigtech, priority: normal }
96
+
97
+ - { name: American Express, section: finance, priority: normal }
98
+ - { name: Bank of America, section: finance, priority: normal }
99
+ - { name: BlackRock, section: finance, priority: normal }
100
+ - { name: Bloomberg, section: finance, priority: normal }
101
+ - { name: BNP Paribas, section: finance, priority: normal }
102
+ - { name: Capital One, section: finance, priority: normal }
103
+ - { name: Citigroup, section: finance, priority: normal }
104
+ - { name: Fidelity Investments, section: finance, priority: normal }
105
+ - { name: Goldman Sachs, section: finance, priority: normal }
106
+ - { name: JPMorgan Chase, section: finance, priority: normal }
107
+ - { name: Mastercard, section: finance, priority: normal }
108
+ - { name: Morgan Stanley, section: finance, priority: normal }
109
+ - { name: PayPal, section: finance, priority: normal }
110
+ - { name: Visa, section: finance, priority: normal }
111
+ - { name: Wells Fargo, section: finance, priority: normal }
112
+
113
+ - { name: Accenture, section: consulting, priority: normal, note: not listed, consider }
114
+ - { name: Deloitte, section: consulting, priority: normal }
115
+ - { name: KPMG, section: consulting, priority: normal }
116
+ - { name: PwC, section: consulting, priority: normal }
117
+ - { name: Red Hat, section: consulting, priority: normal }
118
+ - { name: Splunk, section: consulting, priority: normal }
119
+
120
+ - { name: Analog Devices, section: industrial, priority: normal }
121
+ - { name: Anduril Industries, section: industrial, priority: normal }
122
+ - { name: BAE Systems, section: industrial, priority: normal }
123
+ - { name: Ford, section: industrial, priority: low, note: low yield for a Montreal student }
124
+ - { name: General Motors, section: industrial, priority: low, note: low yield for a Montreal student }
125
+ - { name: Lockheed Martin, section: industrial, priority: normal }
126
+ - { name: SpaceX, section: industrial, priority: normal }
127
+ - { name: Waymo, section: industrial, priority: normal }
128
+
129
+ - { name: 1Password, section: security, priority: normal }
130
+ - { name: Akamai, section: security, priority: normal }
131
+ - { name: Arctic Wolf, section: security, priority: normal }
132
+ - { name: Cloudflare, section: security, priority: normal }
133
+ - { name: CrowdStrike, section: security, priority: normal }
134
+ - { name: Fortinet, section: security, priority: normal }
135
+ - { name: Okta, section: security, priority: normal, note: not listed }
136
+ - { name: Palo Alto Networks, section: security, priority: normal }
137
+ - { name: Wiz, section: security, priority: normal }
138
+
139
+ - { name: Affirm, section: startups, priority: normal }
140
+ - { name: Airbnb, section: startups, priority: normal }
141
+ - { name: Atlassian, section: startups, priority: normal }
142
+ - { name: Autodesk, section: startups, priority: normal }
143
+ - { name: Bumble, section: startups, priority: normal }
144
+ - { name: Canva, section: startups, priority: normal }
145
+ - { name: CircleCI, section: startups, priority: normal }
146
+ - { name: Coinbase, section: startups, priority: normal }
147
+ - { name: Databricks, section: startups, priority: normal }
148
+ - { name: Datadog, section: startups, priority: normal }
149
+ - { name: DigitalOcean, section: startups, priority: normal }
150
+ - { name: Discord, section: startups, priority: normal }
151
+ - { name: Docker, section: startups, priority: normal }
152
+ - { name: DoorDash, section: startups, priority: normal }
153
+ - { name: Dropbox, section: startups, priority: normal }
154
+ - { name: Duolingo, section: startups, priority: normal }
155
+ - { name: eBay, section: startups, priority: normal }
156
+ - { name: Expedia, section: startups, priority: normal }
157
+ - { name: Figma, section: startups, priority: normal }
158
+ - { name: GitHub, section: startups, priority: normal }
159
+ - { name: Grammarly, section: startups, priority: normal }
160
+ - { name: HashiCorp, section: startups, priority: normal }
161
+ - { name: Hugging Face, section: startups, priority: normal }
162
+ - { name: Instacart, section: startups, priority: normal }
163
+ - { name: Lyft, section: startups, priority: normal }
164
+ - { name: MongoDB, section: startups, priority: normal }
165
+ - { name: Notion, section: startups, priority: normal }
166
+ - { name: Palantir, section: startups, priority: normal }
167
+ - { name: Pinterest, section: startups, priority: normal }
168
+ - { name: Postman, section: startups, priority: normal }
169
+ - { name: Quora, section: startups, priority: normal }
170
+ - { name: Reddit, section: startups, priority: normal }
171
+ - { name: Roblox, section: startups, priority: normal }
172
+ - { name: Scale AI, section: startups, priority: normal }
173
+ - { name: Snowflake, section: startups, priority: normal }
174
+ - { name: Spotify, section: startups, priority: normal }
175
+ - { name: Stripe, section: startups, priority: normal }
176
+ - { name: Supabase, section: startups, priority: normal }
177
+ - { name: Twilio, section: startups, priority: normal }
178
+ - { name: Unity, section: startups, priority: normal }
179
+ - { name: Wayfair, section: startups, priority: normal }
180
+ - { name: Yelp, section: startups, priority: normal }
181
+ - { name: Zillow, section: startups, priority: normal }
182
+
183
+ - { name: Activision Blizzard, section: gaming, priority: normal }
184
+ - { name: Bandai Namco, section: gaming, priority: low, note: negligible North American CS pipeline }
185
+ - { name: Blackbird Interactive, section: gaming, priority: normal }
186
+ - { name: Bungie, section: gaming, priority: normal }
187
+ - { name: Capcom, section: gaming, priority: low, note: negligible North American CS pipeline }
188
+ - { name: CD Projekt Red, section: gaming, priority: normal }
189
+ - { name: EA, section: gaming, priority: normal }
190
+ - { name: Epic Games, section: gaming, priority: normal }
191
+ - { name: FromSoftware, section: gaming, priority: low, note: negligible North American CS pipeline }
192
+ - { name: Halo Studios, section: gaming, priority: normal }
193
+ - { name: Microsoft Xbox Game Studios, section: gaming, priority: normal }
194
+ - { name: Nintendo, section: gaming, priority: low, note: negligible North American CS pipeline }
195
+ - { name: Riot Games, section: gaming, priority: normal }
196
+ - { name: Square Enix, section: gaming, priority: low, note: negligible North American CS pipeline }
197
+ - { name: Supercell, section: gaming, priority: low, note: negligible North American CS pipeline }
198
+ - { name: Take-Two Interactive, section: gaming, priority: normal }
@@ -0,0 +1,19 @@
1
+ from stage.dedup.identity import (
2
+ SOURCE_PRIORITY,
3
+ MatchKind,
4
+ MatchResult,
5
+ title_canonical,
6
+ would_merge,
7
+ )
8
+ from stage.dedup.resolve import DuplicateLink, rank, resolve_duplicates
9
+
10
+ __all__ = [
11
+ "SOURCE_PRIORITY",
12
+ "DuplicateLink",
13
+ "MatchKind",
14
+ "MatchResult",
15
+ "rank",
16
+ "resolve_duplicates",
17
+ "title_canonical",
18
+ "would_merge",
19
+ ]
@@ -0,0 +1,113 @@
1
+ from dataclasses import dataclass
2
+ from enum import StrEnum
3
+
4
+ from stage.domain import (
5
+ SOURCE_PRIORITY,
6
+ Job,
7
+ Language,
8
+ location_agrees,
9
+ term_agrees,
10
+ )
11
+ from stage.lexicon import (
12
+ division_qualifiers,
13
+ fold,
14
+ generic_company_tokens,
15
+ name_root_tokens,
16
+ role_lexicon,
17
+ )
18
+
19
+ __all__ = [
20
+ "SOURCE_PRIORITY",
21
+ "MatchKind",
22
+ "MatchResult",
23
+ "title_canonical",
24
+ "would_merge",
25
+ ]
26
+
27
+
28
+ class MatchKind(StrEnum):
29
+ NONE = "none"
30
+ URL = "url"
31
+ SAME_LANGUAGE = "same-language"
32
+ CROSS_LANGUAGE = "cross-language"
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class MatchResult:
37
+ kind: MatchKind = MatchKind.NONE
38
+ evidence: str = ""
39
+
40
+ def __bool__(self) -> bool:
41
+ return self.kind is not MatchKind.NONE
42
+
43
+
44
+ def title_canonical(title: str) -> str:
45
+ folded = fold(title)
46
+ if not folded:
47
+ return ""
48
+ padded = f" {folded} "
49
+ hits: list[tuple[int, str]] = []
50
+ for category, phrases in role_lexicon().items():
51
+ matched = [phrase for phrase in phrases if f" {phrase} " in padded]
52
+ if matched:
53
+ hits.append((max(len(phrase) for phrase in matched), category))
54
+ if not hits:
55
+ return ""
56
+ best = max(length for length, _ in hits)
57
+ winners = sorted(category for length, category in hits if length == best)
58
+ return winners[0] if len(winners) == 1 else ""
59
+
60
+
61
+ def _title_tokens(job: Job) -> frozenset[str]:
62
+ return frozenset(fold(job.title_raw).split())
63
+
64
+
65
+ def _qualified_prefix(shorter: tuple[str, ...], longer: tuple[str, ...]) -> bool:
66
+ if not shorter or longer[: len(shorter)] != shorter:
67
+ return False
68
+ if not set(longer[len(shorter) :]) <= division_qualifiers():
69
+ return False
70
+ return bool(set(shorter) - generic_company_tokens())
71
+
72
+
73
+ def _company_matches(left: Job, right: Job) -> bool:
74
+ if fold(left.company) == fold(right.company):
75
+ return True
76
+ first, second = name_root_tokens(left.company), name_root_tokens(right.company)
77
+ if first == second:
78
+ return True
79
+ shorter, longer = sorted((first, second), key=len)
80
+ return _qualified_prefix(shorter, longer)
81
+
82
+
83
+ def would_merge(left: Job, right: Job) -> MatchResult:
84
+ if left.board_key == right.board_key:
85
+ return MatchResult()
86
+
87
+ if left.apply_url_canonical and left.apply_url_canonical == right.apply_url_canonical:
88
+ return MatchResult(MatchKind.URL, left.apply_url_canonical)
89
+
90
+ if not _company_matches(left, right):
91
+ return MatchResult()
92
+
93
+ if not location_agrees(left.location, right.location):
94
+ return MatchResult()
95
+
96
+ left_tokens, right_tokens = _title_tokens(left), _title_tokens(right)
97
+ if left_tokens and left_tokens == right_tokens:
98
+ return MatchResult(MatchKind.SAME_LANGUAGE, " ".join(sorted(left_tokens)))
99
+
100
+ return _cross_language(left, right)
101
+
102
+
103
+ def _cross_language(left: Job, right: Job) -> MatchResult:
104
+ pair = {left.language, right.language}
105
+ if Language.EN not in pair or Language.FR not in pair:
106
+ return MatchResult()
107
+ if not term_agrees(left.term, right.term):
108
+ return MatchResult()
109
+
110
+ canonical = title_canonical(left.title_raw)
111
+ if not canonical or canonical != title_canonical(right.title_raw):
112
+ return MatchResult()
113
+ return MatchResult(MatchKind.CROSS_LANGUAGE, canonical)
stage/dedup/resolve.py ADDED
@@ -0,0 +1,97 @@
1
+ from collections.abc import Sequence
2
+ from dataclasses import dataclass
3
+
4
+ from stage.dedup.identity import MatchKind, would_merge
5
+ from stage.domain import Job, source_rank
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class DuplicateLink:
10
+ duplicate_id: str
11
+ canonical_id: str
12
+ kind: str
13
+ evidence: str
14
+
15
+
16
+ def rank(job: Job) -> tuple[int, str]:
17
+ return source_rank(job.source, job.id)
18
+
19
+
20
+ def _ambiguous_urls(jobs: Sequence[Job]) -> frozenset[str]:
21
+ seen: dict[tuple[str, str], int] = {}
22
+ for job in jobs:
23
+ if not job.apply_url_canonical:
24
+ continue
25
+ key = (job.source, job.apply_url_canonical)
26
+ seen[key] = seen.get(key, 0) + 1
27
+ return frozenset(url for (_, url), count in seen.items() if count > 1)
28
+
29
+
30
+ def resolve_duplicates(
31
+ incoming: Sequence[Job], existing: Sequence[Job]
32
+ ) -> tuple[DuplicateLink, ...]:
33
+ pool: dict[str, Job] = {job.id: job for job in existing}
34
+ for job in incoming:
35
+ pool[job.id] = job
36
+ jobs = sorted(pool.values(), key=rank)
37
+ ambiguous = _ambiguous_urls(jobs)
38
+
39
+ parent: dict[str, str] = {job.id: job.id for job in jobs}
40
+ boards: dict[str, set[str]] = {job.id: {job.board_key} for job in jobs}
41
+ sources: dict[str, set[str]] = {job.id: {job.source} for job in jobs}
42
+ reason: dict[str, tuple[str, str]] = {}
43
+
44
+ def find(job_id: str) -> str:
45
+ while parent[job_id] != job_id:
46
+ parent[job_id] = parent[parent[job_id]]
47
+ job_id = parent[job_id]
48
+ return job_id
49
+
50
+ order = {job.id: index for index, job in enumerate(jobs)}
51
+
52
+ def union(left_id: str, right_id: str) -> None:
53
+ left_root, right_root = find(left_id), find(right_id)
54
+ if left_root == right_root:
55
+ return
56
+ if order[right_root] < order[left_root]:
57
+ left_root, right_root = right_root, left_root
58
+ parent[right_root] = left_root
59
+ boards[left_root] |= boards[right_root]
60
+ sources[left_root] |= sources[right_root]
61
+
62
+ for job in jobs:
63
+ if job.duplicate_of and job.duplicate_of in parent:
64
+ union(job.id, job.duplicate_of)
65
+
66
+ touched = {job.id for job in incoming}
67
+ for index, left in enumerate(jobs):
68
+ for right in jobs[index + 1 :]:
69
+ if left.id not in touched and right.id not in touched:
70
+ continue
71
+ left_root, right_root = find(left.id), find(right.id)
72
+ if left_root == right_root:
73
+ continue
74
+ if boards[left_root] & boards[right_root] or sources[left_root] & sources[right_root]:
75
+ continue
76
+ match = would_merge(left, right)
77
+ if not match or (match.kind is MatchKind.URL and left.apply_url_canonical in ambiguous):
78
+ continue
79
+ union(left.id, right.id)
80
+ demoted = right.id if find(right.id) != right_root else left.id
81
+ reason[demoted] = (match.kind.value, match.evidence)
82
+
83
+ links: list[DuplicateLink] = []
84
+ for job in jobs:
85
+ canonical = find(job.id)
86
+ if canonical == job.id:
87
+ continue
88
+ kind, evidence = reason.get(job.id, ("", ""))
89
+ links.append(
90
+ DuplicateLink(
91
+ duplicate_id=job.id,
92
+ canonical_id=canonical,
93
+ kind=kind,
94
+ evidence=evidence,
95
+ )
96
+ )
97
+ return tuple(links)