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.
- stage/__init__.py +1 -0
- stage/__main__.py +8 -0
- stage/banner.py +32 -0
- stage/bootstrap/__init__.py +0 -0
- stage/bootstrap/openjobs.py +392 -0
- stage/classify/__init__.py +29 -0
- stage/classify/eligibility.py +115 -0
- stage/classify/internship.py +64 -0
- stage/classify/role.py +91 -0
- stage/classify/scope.py +47 -0
- stage/cli/__init__.py +0 -0
- stage/cli/app.py +4 -0
- stage/cli/commands/__init__.py +8 -0
- stage/cli/commands/discovery.py +294 -0
- stage/cli/commands/insight.py +494 -0
- stage/cli/commands/pipeline.py +337 -0
- stage/cli/commands/postings.py +473 -0
- stage/cli/commands/schedule.py +171 -0
- stage/cli/housekeeping.py +64 -0
- stage/cli/logfile.py +56 -0
- stage/cli/notify.py +170 -0
- stage/cli/options.py +678 -0
- stage/cli/render.py +1398 -0
- stage/cli/runlock.py +74 -0
- stage/cli/schedule.py +702 -0
- stage/cli/schedule_state.py +363 -0
- stage/cli/selection.py +83 -0
- stage/cli/serialize.py +196 -0
- stage/companies.py +542 -0
- stage/data/companies/a.yaml +1289 -0
- stage/data/companies/b.yaml +900 -0
- stage/data/companies/c.yaml +1377 -0
- stage/data/companies/d.yaml +497 -0
- stage/data/companies/e.yaml +519 -0
- stage/data/companies/f.yaml +454 -0
- stage/data/companies/g.yaml +601 -0
- stage/data/companies/h.yaml +446 -0
- stage/data/companies/i.yaml +503 -0
- stage/data/companies/j.yaml +138 -0
- stage/data/companies/k.yaml +278 -0
- stage/data/companies/l.yaml +402 -0
- stage/data/companies/m.yaml +937 -0
- stage/data/companies/n.yaml +549 -0
- stage/data/companies/o.yaml +371 -0
- stage/data/companies/other.yaml +58 -0
- stage/data/companies/p.yaml +825 -0
- stage/data/companies/q.yaml +121 -0
- stage/data/companies/r.yaml +583 -0
- stage/data/companies/s.yaml +1140 -0
- stage/data/companies/t.yaml +817 -0
- stage/data/companies/u.yaml +196 -0
- stage/data/companies/v.yaml +325 -0
- stage/data/companies/w.yaml +353 -0
- stage/data/companies/x.yaml +67 -0
- stage/data/companies/y.yaml +36 -0
- stage/data/companies/z.yaml +146 -0
- stage/data/fonts/DejaVuSans.LICENSE.txt +99 -0
- stage/data/fonts/DejaVuSans.ttf +0 -0
- stage/data/lexicon/company_tokens.yaml +228 -0
- stage/data/lexicon/eligibility.yaml +455 -0
- stage/data/lexicon/inclusive_suffixes.yaml +37 -0
- stage/data/lexicon/internship.yaml +187 -0
- stage/data/lexicon/language.yaml +226 -0
- stage/data/lexicon/locations.yaml +1159 -0
- stage/data/lexicon/roles.yaml +2012 -0
- stage/data/lexicon/terms.yaml +76 -0
- stage/data/lexicon/workday_facets.yaml +27 -0
- stage/data/seed_companies.yaml +198 -0
- stage/dedup/__init__.py +19 -0
- stage/dedup/identity.py +113 -0
- stage/dedup/resolve.py +97 -0
- stage/domain/__init__.py +244 -0
- stage/domain/company.py +49 -0
- stage/domain/coverage.py +86 -0
- stage/domain/custom_board.py +92 -0
- stage/domain/discovery.py +94 -0
- stage/domain/enums.py +114 -0
- stage/domain/events.py +204 -0
- stage/domain/filters.py +27 -0
- stage/domain/health.py +169 -0
- stage/domain/ids.py +48 -0
- stage/domain/job.py +47 -0
- stage/domain/matching.py +15 -0
- stage/domain/priority.py +34 -0
- stage/domain/quarantine.py +39 -0
- stage/domain/rate_state.py +78 -0
- stage/domain/retention.py +20 -0
- stage/domain/rotation.py +46 -0
- stage/domain/signals.py +12 -0
- stage/domain/sync_run.py +35 -0
- stage/domain/text.py +113 -0
- stage/domain/validator.py +14 -0
- stage/domain/visits.py +60 -0
- stage/domain/workday.py +38 -0
- stage/http/__init__.py +58 -0
- stage/http/breaker.py +53 -0
- stage/http/cache.py +44 -0
- stage/http/client.py +725 -0
- stage/http/profiles.py +101 -0
- stage/lexicon.py +370 -0
- stage/normalize/__init__.py +16 -0
- stage/normalize/language.py +47 -0
- stage/normalize/location.py +271 -0
- stage/normalize/terms.py +153 -0
- stage/normalize/urls.py +122 -0
- stage/paths.py +86 -0
- stage/py.typed +0 -0
- stage/services/__init__.py +0 -0
- stage/services/canary.py +120 -0
- stage/services/coverage.py +231 -0
- stage/services/discover.py +747 -0
- stage/services/export.py +274 -0
- stage/services/health.py +237 -0
- stage/services/maintenance.py +225 -0
- stage/services/quarantine.py +20 -0
- stage/services/query.py +86 -0
- stage/services/sync.py +1257 -0
- stage/sources/__init__.py +82 -0
- stage/sources/_text.py +79 -0
- stage/sources/ashby.py +93 -0
- stage/sources/bamboohr.py +80 -0
- stage/sources/base.py +225 -0
- stage/sources/breezy.py +90 -0
- stage/sources/collage.py +60 -0
- stage/sources/community_feeds.py +142 -0
- stage/sources/curated_markdown.py +289 -0
- stage/sources/custom_json.py +610 -0
- stage/sources/espresso.py +154 -0
- stage/sources/feed.py +44 -0
- stage/sources/greenhouse.py +104 -0
- stage/sources/jobbank.py +147 -0
- stage/sources/jobvite.py +133 -0
- stage/sources/lever.py +76 -0
- stage/sources/oracle_cloud.py +187 -0
- stage/sources/platforms.py +609 -0
- stage/sources/quebec_emploi.py +146 -0
- stage/sources/recruitee.py +96 -0
- stage/sources/simplify.py +110 -0
- stage/sources/smartrecruiters.py +216 -0
- stage/sources/speedyapply.py +200 -0
- stage/sources/themuse.py +157 -0
- stage/sources/workable.py +83 -0
- stage/sources/workday.py +524 -0
- stage/sources/zshah.py +99 -0
- stage/storage/__init__.py +29 -0
- stage/storage/migrations/0001_initial.sql +239 -0
- stage/storage/migrations/__init__.py +135 -0
- stage/storage/repository.py +213 -0
- stage/storage/search.py +28 -0
- stage/storage/sqlite_repo.py +1586 -0
- stage/storage/writer.py +249 -0
- stage/tui/__init__.py +0 -0
- stage/tui/app.py +82 -0
- stage/tui/help.py +26 -0
- stage/tui/safe.py +21 -0
- stage/tui/screens/__init__.py +0 -0
- stage/tui/screens/boards.py +186 -0
- stage/tui/screens/postings.py +509 -0
- stage/tui/screens/review.py +209 -0
- stage/tui/screens/splash.py +37 -0
- stage/tui/screens/stats.py +124 -0
- stage/tui/screens/sync.py +194 -0
- stage/tui/state.py +160 -0
- stage/tui/theme.tcss +205 -0
- stage/tui/widgets/__init__.py +0 -0
- stage_cli-1.0.0.dist-info/METADATA +379 -0
- stage_cli-1.0.0.dist-info/RECORD +170 -0
- stage_cli-1.0.0.dist-info/WHEEL +4 -0
- stage_cli-1.0.0.dist-info/entry_points.txt +2 -0
- stage_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
stage/services/canary.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
from stage.domain import (
|
|
5
|
+
Company,
|
|
6
|
+
CompanyFailed,
|
|
7
|
+
CompanyFinished,
|
|
8
|
+
CompanyUnchanged,
|
|
9
|
+
Platform,
|
|
10
|
+
)
|
|
11
|
+
from stage.storage import AsyncRepository
|
|
12
|
+
|
|
13
|
+
AKAMAI_PLATFORMS = frozenset({Platform.WORKDAY})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class BoardProbe:
|
|
18
|
+
source: str
|
|
19
|
+
company: str
|
|
20
|
+
fetched: int = 0
|
|
21
|
+
error: str = ""
|
|
22
|
+
degraded: str = ""
|
|
23
|
+
unchanged: bool = False
|
|
24
|
+
unreachable: bool = False
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def is_failure(self) -> bool:
|
|
28
|
+
return bool(self.error) and not self.unreachable
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def is_unreachable(self) -> bool:
|
|
32
|
+
return bool(self.error) and self.unreachable
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def is_empty(self) -> bool:
|
|
36
|
+
return not self.error and not self.unchanged and self.fetched == 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class CanaryReport:
|
|
41
|
+
probes: tuple[BoardProbe, ...] = field(default_factory=tuple)
|
|
42
|
+
skipped_platforms: tuple[str, ...] = field(default_factory=tuple)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def failures(self) -> tuple[BoardProbe, ...]:
|
|
46
|
+
return tuple(probe for probe in self.probes if probe.is_failure)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def empties(self) -> tuple[BoardProbe, ...]:
|
|
50
|
+
return tuple(probe for probe in self.probes if probe.is_empty)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def unreachable(self) -> tuple[BoardProbe, ...]:
|
|
54
|
+
return tuple(probe for probe in self.probes if probe.is_unreachable)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def passed(self) -> bool:
|
|
58
|
+
return not (self.failures or self.empties)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _probe_key(company: Company) -> tuple[str, str]:
|
|
62
|
+
if company.platform is Platform.CUSTOM_JSON and company.custom is not None:
|
|
63
|
+
return (company.platform.value, company.custom.fmt)
|
|
64
|
+
return (company.platform.value, "")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def select_probes(
|
|
68
|
+
companies: Sequence[Company], *, exclude: frozenset[Platform] = AKAMAI_PLATFORMS
|
|
69
|
+
) -> tuple[list[Company], tuple[str, ...]]:
|
|
70
|
+
chosen: dict[tuple[str, str], Company] = {}
|
|
71
|
+
for company in sorted(
|
|
72
|
+
(entry for entry in companies if entry.enabled),
|
|
73
|
+
key=lambda entry: entry.registry_key,
|
|
74
|
+
):
|
|
75
|
+
if company.platform in exclude:
|
|
76
|
+
continue
|
|
77
|
+
chosen.setdefault(_probe_key(company), company)
|
|
78
|
+
return (
|
|
79
|
+
[chosen[key] for key in sorted(chosen)],
|
|
80
|
+
tuple(sorted(platform.value for platform in exclude)),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def canary(
|
|
85
|
+
repository: AsyncRepository,
|
|
86
|
+
companies: Sequence[Company],
|
|
87
|
+
*,
|
|
88
|
+
exclude: frozenset[Platform] = AKAMAI_PLATFORMS,
|
|
89
|
+
) -> CanaryReport:
|
|
90
|
+
from stage.services.sync import sync
|
|
91
|
+
|
|
92
|
+
selected, skipped = select_probes(companies, exclude=exclude)
|
|
93
|
+
probes: dict[tuple[str, str], BoardProbe] = {}
|
|
94
|
+
|
|
95
|
+
async for event in sync(repository, selected):
|
|
96
|
+
if isinstance(event, CompanyFinished):
|
|
97
|
+
probes[(event.source, event.company)] = BoardProbe(
|
|
98
|
+
source=event.source,
|
|
99
|
+
company=event.company,
|
|
100
|
+
fetched=event.fetched,
|
|
101
|
+
degraded=event.degraded,
|
|
102
|
+
)
|
|
103
|
+
elif isinstance(event, CompanyFailed):
|
|
104
|
+
probes[(event.source, event.company)] = BoardProbe(
|
|
105
|
+
source=event.source,
|
|
106
|
+
company=event.company,
|
|
107
|
+
error=event.error,
|
|
108
|
+
unreachable=event.unreachable,
|
|
109
|
+
)
|
|
110
|
+
elif isinstance(event, CompanyUnchanged):
|
|
111
|
+
probes[(event.source, event.company)] = BoardProbe(
|
|
112
|
+
source=event.source, company=event.company, unchanged=True
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return CanaryReport(
|
|
116
|
+
probes=tuple(probes[key] for key in sorted(probes)), skipped_platforms=skipped
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = ["AKAMAI_PLATFORMS", "BoardProbe", "CanaryReport", "canary", "select_probes"]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from stage.companies import board_label
|
|
6
|
+
from stage.domain import (
|
|
7
|
+
STALE_AFTER_DAYS,
|
|
8
|
+
Company,
|
|
9
|
+
CoverageClassification,
|
|
10
|
+
CoverageRow,
|
|
11
|
+
CoverageState,
|
|
12
|
+
RejectionReason,
|
|
13
|
+
SourceVisit,
|
|
14
|
+
UnregisteredCompany,
|
|
15
|
+
coverage_state,
|
|
16
|
+
)
|
|
17
|
+
from stage.lexicon import fold
|
|
18
|
+
from stage.services.discover import GENERIC_TOKENS, name_matches, name_tokens
|
|
19
|
+
from stage.storage import AsyncRepository
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class CoverageReport:
|
|
24
|
+
rows: tuple[CoverageRow, ...]
|
|
25
|
+
unregistered: tuple[UnregisteredCompany, ...]
|
|
26
|
+
classifications: tuple[CoverageClassification, ...]
|
|
27
|
+
contradictions: tuple[tuple[CoverageClassification, str], ...]
|
|
28
|
+
enabled: int
|
|
29
|
+
disabled: int
|
|
30
|
+
stale_after_days: int
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def gaps(self) -> tuple[CoverageRow, ...]:
|
|
34
|
+
return tuple(row for row in self.rows if row.is_gap)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def producing(self) -> tuple[CoverageRow, ...]:
|
|
38
|
+
return tuple(row for row in self.rows if row.state is CoverageState.PRODUCING)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _board_key(company: Company) -> str | None:
|
|
42
|
+
from stage.sources import adapter_for_platform
|
|
43
|
+
|
|
44
|
+
adapter = adapter_for_platform(company.platform)
|
|
45
|
+
if adapter is None:
|
|
46
|
+
return None
|
|
47
|
+
try:
|
|
48
|
+
return adapter.board_key(company)
|
|
49
|
+
except Exception:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _registry_rows(
|
|
54
|
+
companies: Sequence[Company],
|
|
55
|
+
postings: dict[str, int],
|
|
56
|
+
visits: dict[tuple[str, str], SourceVisit],
|
|
57
|
+
now: datetime,
|
|
58
|
+
stale_after_days: int,
|
|
59
|
+
) -> tuple[CoverageRow, ...]:
|
|
60
|
+
rows: list[CoverageRow] = []
|
|
61
|
+
for company in companies:
|
|
62
|
+
board = _board_key(company)
|
|
63
|
+
if board is None:
|
|
64
|
+
rows.append(
|
|
65
|
+
CoverageRow(
|
|
66
|
+
company=company.name,
|
|
67
|
+
platform=company.platform.value,
|
|
68
|
+
board=board_label(company),
|
|
69
|
+
state=CoverageState.UNROUTABLE,
|
|
70
|
+
postings=0,
|
|
71
|
+
last_success_at=None,
|
|
72
|
+
consecutive_failures=0,
|
|
73
|
+
last_error="",
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
continue
|
|
77
|
+
source, _, _ = board.partition(":")
|
|
78
|
+
visit = visits.get((source, board))
|
|
79
|
+
count = postings.get(board, 0)
|
|
80
|
+
rows.append(
|
|
81
|
+
CoverageRow(
|
|
82
|
+
company=company.name,
|
|
83
|
+
platform=company.platform.value,
|
|
84
|
+
board=board,
|
|
85
|
+
state=coverage_state(
|
|
86
|
+
count,
|
|
87
|
+
visit is not None,
|
|
88
|
+
visit.last_success_at if visit else None,
|
|
89
|
+
visit.consecutive_failures if visit else 0,
|
|
90
|
+
now,
|
|
91
|
+
stale_after_days,
|
|
92
|
+
),
|
|
93
|
+
postings=count,
|
|
94
|
+
last_success_at=visit.last_success_at if visit else None,
|
|
95
|
+
consecutive_failures=visit.consecutive_failures if visit else 0,
|
|
96
|
+
last_error=visit.last_error if visit else "",
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
return tuple(sorted(rows, key=lambda row: (row.state.value, row.company.lower())))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
INTERNSHIP_EVIDENCE_REASONS = frozenset(
|
|
103
|
+
{
|
|
104
|
+
RejectionReason.OUT_OF_SCOPE_DEGREE.value,
|
|
105
|
+
RejectionReason.NOT_A_CS_ROLE.value,
|
|
106
|
+
RejectionReason.UNKNOWN_CS_ROLE.value,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _unregistered(
|
|
112
|
+
companies: Sequence[Company],
|
|
113
|
+
seen: dict[str, dict[str, int]],
|
|
114
|
+
rejected: dict[str, dict[str, int]],
|
|
115
|
+
reasons: dict[str, dict[str, int]],
|
|
116
|
+
classifications: Sequence[CoverageClassification],
|
|
117
|
+
) -> tuple[UnregisteredCompany, ...]:
|
|
118
|
+
exact_names = {name_tokens(company.name) for company in companies}
|
|
119
|
+
index: dict[str, list[str]] = {}
|
|
120
|
+
for company in companies:
|
|
121
|
+
for token in name_tokens(company.name) - GENERIC_TOKENS:
|
|
122
|
+
index.setdefault(token, []).append(company.name)
|
|
123
|
+
|
|
124
|
+
unknown: list[UnregisteredCompany] = []
|
|
125
|
+
for name in {**rejected, **seen}:
|
|
126
|
+
tokens = name_tokens(name)
|
|
127
|
+
if tokens in exact_names:
|
|
128
|
+
continue
|
|
129
|
+
nearby = {
|
|
130
|
+
candidate for token in tokens - GENERIC_TOKENS for candidate in index.get(token, ())
|
|
131
|
+
}
|
|
132
|
+
if any(name_matches(candidate, name) for candidate in nearby):
|
|
133
|
+
continue
|
|
134
|
+
if any(name_matches(entry.company, name) for entry in classifications):
|
|
135
|
+
continue
|
|
136
|
+
sources = seen.get(name, {})
|
|
137
|
+
rejections = reasons.get(name, {})
|
|
138
|
+
unknown.append(
|
|
139
|
+
UnregisteredCompany(
|
|
140
|
+
company=name,
|
|
141
|
+
sources=tuple(sorted(sources or rejected.get(name, {}))),
|
|
142
|
+
postings=sum(sources.values()),
|
|
143
|
+
quarantined=sum(rejected.get(name, {}).values()),
|
|
144
|
+
posts_internships=any(
|
|
145
|
+
count > 0
|
|
146
|
+
for reason, count in rejections.items()
|
|
147
|
+
if reason in INTERNSHIP_EVIDENCE_REASONS
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
return tuple(
|
|
152
|
+
sorted(
|
|
153
|
+
unknown,
|
|
154
|
+
key=lambda row: (
|
|
155
|
+
-row.postings,
|
|
156
|
+
not row.posts_internships,
|
|
157
|
+
-row.quarantined,
|
|
158
|
+
row.company.lower(),
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
REVIEW_STALE_DAYS = 30
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _contradiction(
|
|
168
|
+
record: CoverageClassification,
|
|
169
|
+
registered: Mapping[str, Company],
|
|
170
|
+
moment: datetime,
|
|
171
|
+
) -> str:
|
|
172
|
+
company = registered.get(fold(record.company))
|
|
173
|
+
if company is not None and company.enabled:
|
|
174
|
+
return f"the registry now polls it on {company.platform.value}"
|
|
175
|
+
if company is not None:
|
|
176
|
+
return f"a disabled {company.platform.value} row exists for it"
|
|
177
|
+
age = (moment - record.checked_on).days
|
|
178
|
+
if age >= REVIEW_STALE_DAYS:
|
|
179
|
+
return f"the verdict is {age} days old and nothing has re-derived it"
|
|
180
|
+
return ""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def contradicted_reviews(
|
|
184
|
+
records: Sequence[CoverageClassification],
|
|
185
|
+
companies: Sequence[Company],
|
|
186
|
+
*,
|
|
187
|
+
now: datetime | None = None,
|
|
188
|
+
) -> tuple[tuple[CoverageClassification, str], ...]:
|
|
189
|
+
moment = now or datetime.now(UTC)
|
|
190
|
+
registered = {fold(company.name): company for company in companies}
|
|
191
|
+
found = [
|
|
192
|
+
(record, reason)
|
|
193
|
+
for record in records
|
|
194
|
+
if (reason := _contradiction(record, registered, moment))
|
|
195
|
+
]
|
|
196
|
+
return tuple(sorted(found, key=lambda pair: pair[0].company.casefold()))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def coverage(
|
|
200
|
+
repository: AsyncRepository,
|
|
201
|
+
companies: Sequence[Company],
|
|
202
|
+
*,
|
|
203
|
+
now: datetime | None = None,
|
|
204
|
+
stale_after_days: int = STALE_AFTER_DAYS,
|
|
205
|
+
unregistered: bool = False,
|
|
206
|
+
) -> CoverageReport:
|
|
207
|
+
moment = now or datetime.now(UTC)
|
|
208
|
+
enabled = [company for company in companies if company.enabled]
|
|
209
|
+
postings = await repository.board_counts()
|
|
210
|
+
visits = {(visit.source, visit.board): visit for visit in await repository.all_visits()}
|
|
211
|
+
|
|
212
|
+
seen = await repository.company_counts() if unregistered else {}
|
|
213
|
+
rejected = await repository.quarantine_company_counts() if unregistered else {}
|
|
214
|
+
reasons = await repository.quarantine_company_reasons() if unregistered else {}
|
|
215
|
+
classifications = await repository.coverage_classifications()
|
|
216
|
+
return CoverageReport(
|
|
217
|
+
contradictions=contradicted_reviews(classifications, companies, now=moment),
|
|
218
|
+
rows=_registry_rows(enabled, postings, visits, moment, stale_after_days),
|
|
219
|
+
unregistered=(
|
|
220
|
+
_unregistered(companies, seen, rejected, reasons, classifications)
|
|
221
|
+
if unregistered
|
|
222
|
+
else ()
|
|
223
|
+
),
|
|
224
|
+
classifications=tuple(classifications),
|
|
225
|
+
enabled=len(enabled),
|
|
226
|
+
disabled=len(companies) - len(enabled),
|
|
227
|
+
stale_after_days=stale_after_days,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
__all__ = ["CoverageReport", "contradicted_reviews", "coverage"]
|