jumbotron 0.1.1
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +5 -0
- data/app/adapters/jumbotron/adapters/base.rb +136 -0
- data/app/adapters/jumbotron/adapters/cadence.rb +42 -0
- data/app/adapters/jumbotron/adapters/discovery_definition.rb +7 -0
- data/app/adapters/jumbotron/adapters/espn/nfl.rb +169 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/acquisition_scope.rb +29 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/competition_odds.rb +185 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/lifecycle.rb +30 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/line_acquisition_scope.rb +26 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/progress.rb +54 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/scoreboard.rb +146 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/season_phase.rb +33 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/teams.rb +35 -0
- data/app/adapters/jumbotron/adapters/line_acquisition.rb +19 -0
- data/app/adapters/jumbotron/adapters/operation.rb +23 -0
- data/app/adapters/jumbotron/adapters/policy_definition.rb +14 -0
- data/app/adapters/jumbotron/adapters/registry.rb +78 -0
- data/app/adapters/jumbotron/adapters.rb +11 -0
- data/app/boot/jumbotron/boot/enqueue_discoveries.rb +41 -0
- data/app/clients/jumbotron/clients/espn/competition_odds/get.rb +26 -0
- data/app/clients/jumbotron/clients/espn/competition_odds/get_input.rb +16 -0
- data/app/clients/jumbotron/clients/espn/scoreboard/get.rb +18 -0
- data/app/clients/jumbotron/clients/espn/scoreboard/get_input.rb +17 -0
- data/app/clients/jumbotron/clients/espn/teams/get.rb +18 -0
- data/app/clients/jumbotron/clients/espn/teams/get_input.rb +15 -0
- data/app/clients/jumbotron/clients/espn.rb +44 -0
- data/app/clients/jumbotron/clients.rb +8 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition.rb +79 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/draw_odds.rb +31 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/get.rb +66 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/item.rb +60 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/listed_provider.rb +31 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/market_window.rb +31 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/point_spread.rb +27 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/price.rb +35 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/side_quote.rb +35 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/team_odds.rb +40 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb +49 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/event.rb +81 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/scoreboard/get.rb +88 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/season.rb +27 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/status.rb +69 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/support.rb +61 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/team.rb +42 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/teams/get.rb +76 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/venue.rb +34 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/week.rb +23 -0
- data/app/deserializers/jumbotron/deserializers/public/game.rb +26 -0
- data/app/deserializers/jumbotron/deserializers/public/game_ids.rb +28 -0
- data/app/deserializers/jumbotron/deserializers/public/includes.rb +32 -0
- data/app/deserializers/jumbotron/deserializers/public/schedule.rb +151 -0
- data/app/deserializers/jumbotron/deserializers/public/schedule_groups.rb +91 -0
- data/app/errors/jumbotron/error.rb +6 -0
- data/app/errors/jumbotron/errors/adapters.rb +143 -0
- data/app/errors/jumbotron/errors/canonical.rb +67 -0
- data/app/errors/jumbotron/errors/public.rb +27 -0
- data/app/errors/jumbotron/invalid_request_error.rb +6 -0
- data/app/errors/jumbotron/not_found_error.rb +6 -0
- data/app/errors/jumbotron/unsupported_capability_error.rb +6 -0
- data/app/jobs/jumbotron/application_job.rb +6 -0
- data/app/jobs/jumbotron/discovery_job.rb +14 -0
- data/app/jobs/jumbotron/game_update_job.rb +12 -0
- data/app/jobs/jumbotron/line_update_job.rb +12 -0
- data/app/models/jumbotron/application_record.rb +8 -0
- data/app/models/jumbotron/bookmaker.rb +11 -0
- data/app/models/jumbotron/canonical.rb +128 -0
- data/app/models/jumbotron/game.rb +92 -0
- data/app/models/jumbotron/game_participant.rb +10 -0
- data/app/models/jumbotron/historical_change.rb +9 -0
- data/app/models/jumbotron/league.rb +12 -0
- data/app/models/jumbotron/line_observation.rb +18 -0
- data/app/models/jumbotron/observation_batch.rb +14 -0
- data/app/models/jumbotron/provider_identity.rb +32 -0
- data/app/models/jumbotron/public/translate_outcome.rb +57 -0
- data/app/models/jumbotron/public.rb +88 -0
- data/app/models/jumbotron/schedule_group.rb +22 -0
- data/app/models/jumbotron/season.rb +13 -0
- data/app/models/jumbotron/season_phase.rb +12 -0
- data/app/models/jumbotron/sport.rb +10 -0
- data/app/models/jumbotron/team.rb +11 -0
- data/app/models/jumbotron/venue.rb +10 -0
- data/app/providers/jumbotron/providers/espn/cooldown.rb +88 -0
- data/app/providers/jumbotron/providers/espn/cooldown_active_error.rb +13 -0
- data/app/providers/jumbotron/providers/espn/provider_failure.rb +40 -0
- data/app/providers/jumbotron/providers/espn/resources/competition_odds.rb +29 -0
- data/app/providers/jumbotron/providers/espn/resources/scoreboard.rb +90 -0
- data/app/providers/jumbotron/providers/espn/resources/teams.rb +19 -0
- data/app/scheduling/jumbotron/scheduling/backends/sidekiq_cron.rb +70 -0
- data/app/scheduling/jumbotron/scheduling/backends/solid_queue.rb +92 -0
- data/app/scheduling/jumbotron/scheduling/clock.rb +61 -0
- data/app/scheduling/jumbotron/scheduling/desired_schedules.rb +51 -0
- data/app/scheduling/jumbotron/scheduling/detect_integration.rb +45 -0
- data/app/scheduling/jumbotron/scheduling/materializer.rb +13 -0
- data/app/scheduling/jumbotron/scheduling/owned.rb +19 -0
- data/app/scheduling/jumbotron/scheduling/schedule_definition.rb +7 -0
- data/app/serializers/jumbotron/serializers/clients/espn/competition_odds/get.rb +17 -0
- data/app/serializers/jumbotron/serializers/clients/espn/scoreboard/get.rb +22 -0
- data/app/serializers/jumbotron/serializers/clients/espn/teams/get.rb +20 -0
- data/app/services/jumbotron/services/adapters/build_acquisition_scopes.rb +37 -0
- data/app/services/jumbotron/services/adapters/build_line_acquisition_scopes.rb +39 -0
- data/app/services/jumbotron/services/adapters/ensure_league.rb +29 -0
- data/app/services/jumbotron/services/adapters/execute_endpoint.rb +49 -0
- data/app/services/jumbotron/services/adapters/execute_line_acquisition_scope.rb +75 -0
- data/app/services/jumbotron/services/adapters/execute_synchronization_scope.rb +64 -0
- data/app/services/jumbotron/services/adapters/resolve_discovery.rb +53 -0
- data/app/services/jumbotron/services/adapters/resolve_game_update_policy.rb +74 -0
- data/app/services/jumbotron/services/adapters/resolve_line_update_policy.rb +74 -0
- data/app/services/jumbotron/services/adapters/select_eligible_games.rb +41 -0
- data/app/services/jumbotron/services/adapters/validate_sync_request.rb +51 -0
- data/app/services/jumbotron/services/canonical/attach_provider_identities.rb +59 -0
- data/app/services/jumbotron/services/canonical/derive_consensus_for_games.rb +18 -0
- data/app/services/jumbotron/services/canonical/derive_consensus_line.rb +70 -0
- data/app/services/jumbotron/services/canonical/ensure_bookmaker.rb +70 -0
- data/app/services/jumbotron/services/canonical/ensure_schedule_group.rb +40 -0
- data/app/services/jumbotron/services/canonical/ensure_season.rb +22 -0
- data/app/services/jumbotron/services/canonical/ensure_season_phase.rb +22 -0
- data/app/services/jumbotron/services/canonical/persist_line_observations.rb +145 -0
- data/app/services/jumbotron/services/canonical/record_observation_evidence.rb +51 -0
- data/app/services/jumbotron/services/canonical/resolve_current_lines.rb +21 -0
- data/app/services/jumbotron/services/canonical/resolve_current_lines_for_games.rb +69 -0
- data/app/services/jumbotron/services/canonical/upsert_game.rb +186 -0
- data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +88 -0
- data/app/services/jumbotron/services/canonical/upsert_team.rb +90 -0
- data/app/services/jumbotron/services/canonical/upsert_venue.rb +92 -0
- data/app/services/jumbotron/services/public/assemble_public_game.rb +131 -0
- data/app/services/jumbotron/services/public/assemble_public_schedule.rb +63 -0
- data/app/services/jumbotron/services/public/derive_consensus_for_games.rb +21 -0
- data/app/services/jumbotron/services/public/load_games_by_ids.rb +18 -0
- data/app/services/jumbotron/services/public/load_included_lines.rb +47 -0
- data/app/services/jumbotron/services/public/map_public_consensus_lines.rb +44 -0
- data/app/services/jumbotron/services/public/map_public_current_lines.rb +27 -0
- data/app/services/jumbotron/services/public/read_game.rb +32 -0
- data/app/services/jumbotron/services/public/read_schedule.rb +116 -0
- data/app/services/jumbotron/services/public/read_schedule_groups.rb +90 -0
- data/app/services/jumbotron/services/public/resolve_current_lines_for_games.rb +21 -0
- data/app/synchronization/jumbotron/synchronization/lease.rb +127 -0
- data/app/workflows/jumbotron/workflows/execute_discovery_workflow.rb +131 -0
- data/app/workflows/jumbotron/workflows/execute_game_update_policy_workflow.rb +159 -0
- data/app/workflows/jumbotron/workflows/execute_line_update_policy_workflow.rb +156 -0
- data/app/workflows/jumbotron/workflows/public_read.rb +29 -0
- data/app/workflows/jumbotron/workflows/read_consensus_workflow.rb +37 -0
- data/app/workflows/jumbotron/workflows/read_current_lines_workflow.rb +27 -0
- data/app/workflows/jumbotron/workflows/read_game_workflow.rb +34 -0
- data/app/workflows/jumbotron/workflows/read_schedule_groups_workflow.rb +21 -0
- data/app/workflows/jumbotron/workflows/read_schedule_workflow.rb +48 -0
- data/app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb +183 -0
- data/db/migrate/20260812000001_create_jumbotron_foundational_catalog.rb +68 -0
- data/db/migrate/20260812000002_create_jumbotron_game_graph.rb +70 -0
- data/db/migrate/20260812000003_add_freshness_to_jumbotron_provider_mutated_entities.rb +31 -0
- data/db/migrate/20260812000004_create_jumbotron_observation_evidence.rb +34 -0
- data/db/migrate/20260813000001_create_jumbotron_schedule_groups.rb +24 -0
- data/db/migrate/20260814000001_create_jumbotron_line_observations.rb +38 -0
- data/db/migrate/20260820000001_add_progress_to_jumbotron_games.rb +14 -0
- data/lib/jumbotron/client.rb +40 -0
- data/lib/jumbotron/engine.rb +32 -0
- data/lib/jumbotron/version.rb +5 -0
- data/lib/jumbotron.rb +13 -0
- data/lib/tasks/jumbotron_schedules.rake +12 -0
- metadata +239 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module Espn
|
|
6
|
+
module NflHelpers
|
|
7
|
+
module Progress
|
|
8
|
+
NIL_LIFECYCLES = %w[scheduled postponed cancelled].freeze
|
|
9
|
+
INTERMISSION_PATTERN = /halftime/i
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(status, lifecycle:)
|
|
14
|
+
return if NIL_LIFECYCLES.include?(lifecycle.to_s)
|
|
15
|
+
return quarter_intermission if intermission?(status)
|
|
16
|
+
|
|
17
|
+
period = status.period
|
|
18
|
+
return if period.nil? || period < 1
|
|
19
|
+
|
|
20
|
+
kind = period <= 4 ? "quarter" : "overtime"
|
|
21
|
+
number = period <= 4 ? period : period - 4
|
|
22
|
+
build(state: "active", kind: kind, number: number, clock: clock_for(status))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def intermission?(status)
|
|
26
|
+
[status.type_name, status.type_description, status.type_detail, status.type_short_detail]
|
|
27
|
+
.compact
|
|
28
|
+
.any? { |value| INTERMISSION_PATTERN.match?(value) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def quarter_intermission
|
|
32
|
+
build(state: "intermission", kind: "quarter", number: 2, clock: nil)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def clock_for(status)
|
|
36
|
+
seconds = status.clock.is_a?(Integer) ? status.clock : nil
|
|
37
|
+
display = status.display_clock.presence
|
|
38
|
+
return if seconds.nil? && display.nil?
|
|
39
|
+
|
|
40
|
+
Canonical::GameClock.new(mode: "remaining", seconds: seconds, display: display)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build(state:, kind:, number:, clock:)
|
|
44
|
+
Canonical::GameProgress.new(
|
|
45
|
+
state: state,
|
|
46
|
+
segment: Canonical::GameSegment.new(kind: kind, number: number),
|
|
47
|
+
clock: clock
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Jumbotron
|
|
6
|
+
module Adapters
|
|
7
|
+
module Espn
|
|
8
|
+
module NflHelpers
|
|
9
|
+
class Scoreboard
|
|
10
|
+
def self.call(board, observed_at:)
|
|
11
|
+
events = board.respond_to?(:events) ? board.events : Array(board)
|
|
12
|
+
Canonical::SyncInput.new(
|
|
13
|
+
observed_at: observed_at,
|
|
14
|
+
games: events.map { |event| transform_event(event) },
|
|
15
|
+
teams: []
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.transform_event(event)
|
|
20
|
+
competition = event.competitions&.first
|
|
21
|
+
raise TransformError, "event #{event.id} missing competition" if competition.nil?
|
|
22
|
+
|
|
23
|
+
status = competition.status || event.status
|
|
24
|
+
raise TransformError, "event #{event.id} missing status" if status.nil?
|
|
25
|
+
|
|
26
|
+
lifecycle = Lifecycle.call(status.type_name)
|
|
27
|
+
season = event.season
|
|
28
|
+
if season.nil? || season.year.nil? || season.type.nil?
|
|
29
|
+
raise TransformError,
|
|
30
|
+
"event #{event.id} missing season"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
participants = Array(competition.competitors)
|
|
34
|
+
raise TransformError, "event #{event.id} requires exactly 2 competitors" unless participants.length == 2
|
|
35
|
+
|
|
36
|
+
Canonical::GameInput.new(
|
|
37
|
+
provider_identities: identities_for(event, competition),
|
|
38
|
+
scheduled_at: parse_time(event.date.presence || competition.date.presence || competition.start_date),
|
|
39
|
+
lifecycle: lifecycle,
|
|
40
|
+
neutral_site: competition.neutral_site,
|
|
41
|
+
season_year: season.year.to_i,
|
|
42
|
+
season_phase_key: SeasonPhase.call(season.type),
|
|
43
|
+
schedule_group: schedule_group_for(event),
|
|
44
|
+
venue: transform_venue(competition.venue),
|
|
45
|
+
participants: participants.map { |c| transform_participant(c, lifecycle) },
|
|
46
|
+
progress: Progress.call(status, lifecycle: lifecycle)
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
private_class_method :transform_event
|
|
50
|
+
|
|
51
|
+
def self.schedule_group_for(event)
|
|
52
|
+
number = event.week&.number
|
|
53
|
+
return if number.nil?
|
|
54
|
+
|
|
55
|
+
Canonical::ScheduleGroupInput.new(
|
|
56
|
+
kind: "week",
|
|
57
|
+
number: number,
|
|
58
|
+
name: "Week #{number}"
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
private_class_method :schedule_group_for
|
|
62
|
+
|
|
63
|
+
def self.identities_for(event, competition)
|
|
64
|
+
ids = []
|
|
65
|
+
if event.id.present?
|
|
66
|
+
ids << Canonical::ProviderIdentityRef.new(provider: "espn", namespace: "event", id: event.id.to_s)
|
|
67
|
+
end
|
|
68
|
+
if competition.id.present?
|
|
69
|
+
ids << Canonical::ProviderIdentityRef.new(provider: "espn", namespace: "competition",
|
|
70
|
+
id: competition.id.to_s)
|
|
71
|
+
end
|
|
72
|
+
raise TransformError, "event/competition provider id required" if ids.empty?
|
|
73
|
+
|
|
74
|
+
ids.uniq
|
|
75
|
+
end
|
|
76
|
+
private_class_method :identities_for
|
|
77
|
+
|
|
78
|
+
def self.transform_venue(venue)
|
|
79
|
+
return nil if venue.nil?
|
|
80
|
+
raise TransformError, "venue id required" if venue.id.blank?
|
|
81
|
+
raise TransformError, "venue name required" if venue.full_name.blank?
|
|
82
|
+
|
|
83
|
+
Canonical::VenueInput.new(
|
|
84
|
+
provider_identities: [
|
|
85
|
+
Canonical::ProviderIdentityRef.new(provider: "espn", namespace: "venue", id: venue.id.to_s)
|
|
86
|
+
],
|
|
87
|
+
name: venue.full_name.to_s
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
private_class_method :transform_venue
|
|
91
|
+
|
|
92
|
+
def self.transform_participant(competitor, lifecycle)
|
|
93
|
+
team = competitor.team
|
|
94
|
+
raise TransformError, "competitor missing team" if team.nil?
|
|
95
|
+
raise TransformError, "team id required" if team.id.blank?
|
|
96
|
+
|
|
97
|
+
name = team.display_name.presence || team.name
|
|
98
|
+
raise TransformError, "team name required" if name.blank?
|
|
99
|
+
raise TransformError, "home_away required" if competitor.home_away.blank?
|
|
100
|
+
|
|
101
|
+
Canonical::ParticipantInput.new(
|
|
102
|
+
provider_identities: [
|
|
103
|
+
Canonical::ProviderIdentityRef.new(provider: "espn", namespace: "team", id: team.id.to_s)
|
|
104
|
+
],
|
|
105
|
+
team_name: name.to_s,
|
|
106
|
+
role: competitor.home_away.to_s,
|
|
107
|
+
score: coerce_score(competitor.score),
|
|
108
|
+
result: coerce_result(competitor.winner, lifecycle)
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
private_class_method :transform_participant
|
|
112
|
+
|
|
113
|
+
def self.coerce_score(raw)
|
|
114
|
+
return nil if raw.nil? || raw == ""
|
|
115
|
+
|
|
116
|
+
Integer(raw)
|
|
117
|
+
rescue ArgumentError, TypeError
|
|
118
|
+
raise TransformError, "invalid score: #{raw.inspect}"
|
|
119
|
+
end
|
|
120
|
+
private_class_method :coerce_score
|
|
121
|
+
|
|
122
|
+
def self.coerce_result(winner, lifecycle)
|
|
123
|
+
return nil unless lifecycle == "completed"
|
|
124
|
+
return nil if winner.nil?
|
|
125
|
+
|
|
126
|
+
winner ? "win" : "loss"
|
|
127
|
+
end
|
|
128
|
+
private_class_method :coerce_result
|
|
129
|
+
|
|
130
|
+
def self.parse_time(raw)
|
|
131
|
+
raise TransformError, "scheduled_at missing" if raw.blank?
|
|
132
|
+
|
|
133
|
+
Time.iso8601(raw.to_s)
|
|
134
|
+
rescue ArgumentError, TypeError
|
|
135
|
+
begin
|
|
136
|
+
Time.zone.parse(raw.to_s) || (raise ArgumentError)
|
|
137
|
+
rescue ArgumentError, TypeError
|
|
138
|
+
raise TransformError, "scheduled_at unparseable: #{raw.inspect}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
private_class_method :parse_time
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module Espn
|
|
6
|
+
module NflHelpers
|
|
7
|
+
module SeasonPhase
|
|
8
|
+
MAP = {
|
|
9
|
+
1 => "preseason",
|
|
10
|
+
2 => "regular_season",
|
|
11
|
+
3 => "postseason",
|
|
12
|
+
4 => "offseason"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def call(season_type)
|
|
18
|
+
mapped = MAP[season_type.to_i]
|
|
19
|
+
raise TransformError, "unsupported ESPN season.type: #{season_type.inspect}" if mapped.nil?
|
|
20
|
+
|
|
21
|
+
mapped
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def espn_season_type(phase_key)
|
|
25
|
+
return if phase_key.nil?
|
|
26
|
+
|
|
27
|
+
MAP.key(phase_key.to_s)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module Espn
|
|
6
|
+
module NflHelpers
|
|
7
|
+
class Teams
|
|
8
|
+
def self.call(teams, observed_at:)
|
|
9
|
+
list = Array(teams)
|
|
10
|
+
Canonical::SyncInput.new(
|
|
11
|
+
observed_at: observed_at,
|
|
12
|
+
games: [],
|
|
13
|
+
teams: list.map { |team| transform_team(team) }
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.transform_team(team)
|
|
18
|
+
raise TransformError, "team id is required" if team.id.nil? || team.id.to_s.empty?
|
|
19
|
+
|
|
20
|
+
name = team.display_name.presence || team.name
|
|
21
|
+
raise TransformError, "team name is required for #{team.id}" if name.nil? || name.to_s.empty?
|
|
22
|
+
|
|
23
|
+
Canonical::TeamInput.new(
|
|
24
|
+
provider_identities: [
|
|
25
|
+
Canonical::ProviderIdentityRef.new(provider: "espn", namespace: "team", id: team.id.to_s)
|
|
26
|
+
],
|
|
27
|
+
name: name.to_s
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
private_class_method :transform_team
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module LineAcquisition
|
|
6
|
+
def line_acquisition_scope_for(_game)
|
|
7
|
+
nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def acquire_line_odds(_acquisition)
|
|
11
|
+
raise ConfigurationError, "#{name} must implement acquire_line_odds"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def translate_line_odds(*)
|
|
15
|
+
raise ConfigurationError, "#{name} must implement translate_line_odds"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
class Operation
|
|
6
|
+
attr_reader :adapter, :resource, :transformer
|
|
7
|
+
|
|
8
|
+
def initialize(adapter:, resource:, transformer:)
|
|
9
|
+
@adapter = adapter
|
|
10
|
+
@resource = resource
|
|
11
|
+
@transformer = transformer
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def acquire(**query)
|
|
15
|
+
resource.acquire(adapter: adapter, **query)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def transform(typed_payload, observed_at:)
|
|
19
|
+
transformer.call(typed_payload, observed_at: observed_at)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
# Frozen registry entry for one adapter-declared update policy. Not a Policy base class.
|
|
6
|
+
PolicyDefinition = Data.define(:id, :type, :cadence, :eligible) do
|
|
7
|
+
def eligible?(game, now:)
|
|
8
|
+
raise ::Jumbotron::Adapters::ConfigurationError, "policy #{id} has no eligible predicate" if eligible.nil?
|
|
9
|
+
|
|
10
|
+
eligible.call(game, now: now)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Jumbotron
|
|
6
|
+
module Adapters
|
|
7
|
+
module Registry
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Stores the adapter class reference (not a config snapshot).
|
|
11
|
+
# Top-of-class register! is safe: identity is the class, not registry_key.
|
|
12
|
+
def register(adapter_class)
|
|
13
|
+
classes[adapter_class.name] = adapter_class
|
|
14
|
+
adapter_class
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def registered
|
|
18
|
+
classes.values
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def registered?(adapter_class)
|
|
22
|
+
classes[adapter_class.name] == adapter_class
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Adapter classes register via load-time `register!`. In non-eager-load
|
|
26
|
+
# processes (e.g. Solid Queue workers), nothing may have constantized them
|
|
27
|
+
# yet when jobs resolve by adapter_id string — load them first.
|
|
28
|
+
# If classes are already loaded but the in-memory registry was cleared,
|
|
29
|
+
# re-register Base subclasses (constantize alone does not re-run file side effects).
|
|
30
|
+
def ensure_loaded!
|
|
31
|
+
Dir[Jumbotron::Engine.root.join("app/adapters/jumbotron/adapters/**/*.rb")].each do |path|
|
|
32
|
+
relative = Pathname.new(path).relative_path_from(Jumbotron::Engine.root.join("app/adapters")).to_s
|
|
33
|
+
klass = relative.chomp(".rb").camelize.constantize
|
|
34
|
+
next unless klass.is_a?(Class)
|
|
35
|
+
next unless klass < Base
|
|
36
|
+
next if klass == Base
|
|
37
|
+
|
|
38
|
+
klass.register! unless registered?(klass)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find(adapter_id)
|
|
43
|
+
id = adapter_id.to_s
|
|
44
|
+
registered.find do |adapter|
|
|
45
|
+
adapter.adapter_id == id
|
|
46
|
+
rescue ConfigurationError
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reset!
|
|
52
|
+
@classes = {}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Raises if two different registered classes share the same registry_key.
|
|
56
|
+
def assert_unique_keys!
|
|
57
|
+
grouped = registered.group_by do |adapter|
|
|
58
|
+
adapter.registry_key
|
|
59
|
+
rescue ConfigurationError
|
|
60
|
+
:incomplete
|
|
61
|
+
end
|
|
62
|
+
grouped.each do |key, adapters|
|
|
63
|
+
next if key == :incomplete
|
|
64
|
+
next if adapters.uniq.size <= 1
|
|
65
|
+
|
|
66
|
+
raise RegistrationError,
|
|
67
|
+
"conflicting adapter registration for #{key.inspect}: " \
|
|
68
|
+
"#{adapters.map(&:name).join(' vs ')}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def classes
|
|
73
|
+
@classes ||= {}
|
|
74
|
+
end
|
|
75
|
+
private_class_method :classes
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
class RegistrationError < StandardError; end
|
|
6
|
+
class TransformError < StandardError; end
|
|
7
|
+
class ConfigurationError < StandardError; end
|
|
8
|
+
class UnknownPolicyError < StandardError; end
|
|
9
|
+
class UnknownDiscoveryError < StandardError; end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Boot
|
|
5
|
+
module EnqueueDiscoveries
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def call
|
|
9
|
+
return unless eligible?
|
|
10
|
+
|
|
11
|
+
enqueue!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def eligible?
|
|
15
|
+
return false unless Jumbotron::Engine.config.boot_discovery
|
|
16
|
+
return false if Rails.env.test?
|
|
17
|
+
return false if defined?(Rails::Console)
|
|
18
|
+
return false if rake_environment?
|
|
19
|
+
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def enqueue!
|
|
24
|
+
Jumbotron::Scheduling::DesiredSchedules.ensure_adapters_loaded!
|
|
25
|
+
Jumbotron::Adapters::Registry.registered.each do |adapter|
|
|
26
|
+
adapter.discovery_ids.each do |discovery_id|
|
|
27
|
+
Jumbotron::DiscoveryJob.perform_later(
|
|
28
|
+
adapter_id: adapter.adapter_id,
|
|
29
|
+
discovery_id: discovery_id.to_s,
|
|
30
|
+
continuation_attempt: 1
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def rake_environment?
|
|
37
|
+
defined?(Rake) && Rake.respond_to?(:application) && Array(Rake.application.top_level_tasks).any?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Clients
|
|
5
|
+
class Espn
|
|
6
|
+
module CompetitionOdds
|
|
7
|
+
class Get < CommandTower::Clients::EndpointBase
|
|
8
|
+
authentication :none
|
|
9
|
+
http_method :get
|
|
10
|
+
path do |input|
|
|
11
|
+
relative = [
|
|
12
|
+
"sports", input.sport, "leagues", input.league,
|
|
13
|
+
"events", input.event_id,
|
|
14
|
+
"competitions", input.competition_id,
|
|
15
|
+
"odds"
|
|
16
|
+
].join("/")
|
|
17
|
+
CommandTower::Clients::Url.join(Jumbotron::Clients::Espn.core_base_url, relative)
|
|
18
|
+
end
|
|
19
|
+
input GetInput
|
|
20
|
+
request_serializer ::Jumbotron::Serializers::Clients::Espn::CompetitionOdds::Get
|
|
21
|
+
response_deserializer ::Jumbotron::Deserializers::Clients::Espn::CompetitionOdds::Get
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Clients
|
|
5
|
+
class Espn
|
|
6
|
+
module CompetitionOdds
|
|
7
|
+
class GetInput < CommandTower::Clients::EndpointInput
|
|
8
|
+
attribute :sport, :string
|
|
9
|
+
attribute :league, :string
|
|
10
|
+
attribute :event_id, :string
|
|
11
|
+
attribute :competition_id, :string
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Clients
|
|
5
|
+
class Espn
|
|
6
|
+
module Scoreboard
|
|
7
|
+
class Get < CommandTower::Clients::EndpointBase
|
|
8
|
+
authentication :none
|
|
9
|
+
http_method :get
|
|
10
|
+
path { |input| "#{input.sport}/#{input.league}/scoreboard" }
|
|
11
|
+
input GetInput
|
|
12
|
+
request_serializer ::Jumbotron::Serializers::Clients::Espn::Scoreboard::Get
|
|
13
|
+
response_deserializer ::Jumbotron::Deserializers::Clients::Espn::Scoreboard::Get
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Clients
|
|
5
|
+
class Espn
|
|
6
|
+
module Scoreboard
|
|
7
|
+
class GetInput < CommandTower::Clients::EndpointInput
|
|
8
|
+
attribute :sport, :string
|
|
9
|
+
attribute :league, :string
|
|
10
|
+
attribute :season_type
|
|
11
|
+
attribute :week
|
|
12
|
+
attribute :dates
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Clients
|
|
5
|
+
class Espn
|
|
6
|
+
module Teams
|
|
7
|
+
class Get < CommandTower::Clients::EndpointBase
|
|
8
|
+
authentication :none
|
|
9
|
+
http_method :get
|
|
10
|
+
path { |input| "#{input.sport}/#{input.league}/teams" }
|
|
11
|
+
input GetInput
|
|
12
|
+
request_serializer ::Jumbotron::Serializers::Clients::Espn::Teams::Get
|
|
13
|
+
response_deserializer ::Jumbotron::Deserializers::Clients::Espn::Teams::Get
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Clients
|
|
5
|
+
class Espn
|
|
6
|
+
module Teams
|
|
7
|
+
class GetInput < CommandTower::Clients::EndpointInput
|
|
8
|
+
attribute :sport, :string
|
|
9
|
+
attribute :league, :string
|
|
10
|
+
attribute :limit
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Jumbotron
|
|
6
|
+
module Clients
|
|
7
|
+
class Espn < CommandTower::Clients::ClientBase
|
|
8
|
+
DEFAULT_BASE_URL = "https://site.api.espn.com/apis/site/v2/sports"
|
|
9
|
+
DEFAULT_CORE_BASE_URL = "https://sports.core.api.espn.com/v2"
|
|
10
|
+
|
|
11
|
+
def self.core_base_url
|
|
12
|
+
ENV.fetch("JUMBOTRON_ESPN_CORE_BASE_URL", DEFAULT_CORE_BASE_URL)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def decode_response(response)
|
|
16
|
+
CommandTower::Clients::DecodedResponse.new(
|
|
17
|
+
payload: parse_json(response.body),
|
|
18
|
+
provider_metadata: {}
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def base_url
|
|
25
|
+
ENV.fetch("JUMBOTRON_ESPN_SITE_BASE_URL", DEFAULT_BASE_URL)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def default_headers
|
|
29
|
+
{ "Accept" => "application/json" }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def parse_json(body)
|
|
35
|
+
JSON.parse(body.to_s)
|
|
36
|
+
rescue JSON::ParserError => e
|
|
37
|
+
raise CommandTower::Clients::Errors::DeserializationError.new(
|
|
38
|
+
message: "ESPN response body is not valid JSON: #{e.message}",
|
|
39
|
+
details: { path: "", expected: "JSON object", actual: "invalid_json", rule: "type", messages: [e.message] }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|