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,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Workflows
|
|
5
|
+
class SynchronizeAdapterWorkflow < CommandTower::Workflows::ApplicationWorkflow
|
|
6
|
+
retry_strategy :none
|
|
7
|
+
|
|
8
|
+
def call(adapter:, endpoint:, league_id:, observed_at: nil, acquisition: {})
|
|
9
|
+
observed_at ||= Time.current
|
|
10
|
+
|
|
11
|
+
validated = Jumbotron::Services::Adapters::ValidateSyncRequest.call(
|
|
12
|
+
adapter: adapter,
|
|
13
|
+
endpoint: endpoint,
|
|
14
|
+
league_id: league_id
|
|
15
|
+
)
|
|
16
|
+
return map_service_failure(validated, failure_class: "validation") unless validated.success?
|
|
17
|
+
|
|
18
|
+
executed = Jumbotron::Services::Adapters::ExecuteEndpoint.call(
|
|
19
|
+
adapter: adapter,
|
|
20
|
+
endpoint: endpoint,
|
|
21
|
+
observed_at: observed_at,
|
|
22
|
+
acquisition: acquisition || {}
|
|
23
|
+
)
|
|
24
|
+
return map_execute_failure(executed) unless executed.success?
|
|
25
|
+
|
|
26
|
+
transaction do
|
|
27
|
+
apply_canonical_sync!(
|
|
28
|
+
input: executed.data[:sync_input],
|
|
29
|
+
league: validated.data[:league],
|
|
30
|
+
provider: adapter.provider,
|
|
31
|
+
adapter_scope: "#{adapter.provider}:#{adapter.sport}:#{adapter.league}:#{endpoint}"
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def apply_canonical_sync!(input:, league:, provider:, adapter_scope:)
|
|
39
|
+
unless input.is_a?(Jumbotron::Canonical::SyncInput)
|
|
40
|
+
fail_transaction!(
|
|
41
|
+
map_service_failure(
|
|
42
|
+
CommandTower::Services::ServiceResult.failure(
|
|
43
|
+
errors: [Jumbotron::Errors::Canonical::InvalidSyncInputError.new]
|
|
44
|
+
),
|
|
45
|
+
failure_class: "mutation"
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
change_set = Jumbotron::Canonical::ChangeSet.new
|
|
51
|
+
teams = []
|
|
52
|
+
|
|
53
|
+
games = input.games.map do |game_input|
|
|
54
|
+
sync_one_game!(game_input, league, input.observed_at, change_set)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
input.teams.each do |team_input|
|
|
58
|
+
team_result = Services::Canonical::UpsertTeam.call(
|
|
59
|
+
team_input: team_input,
|
|
60
|
+
observed_at: input.observed_at,
|
|
61
|
+
change_set: change_set
|
|
62
|
+
)
|
|
63
|
+
fail_on_service_failure!(team_result)
|
|
64
|
+
teams << team_result.data[:team]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
evidence = Services::Canonical::RecordObservationEvidence.call(
|
|
68
|
+
change_set: change_set,
|
|
69
|
+
provider: provider,
|
|
70
|
+
adapter_scope: adapter_scope,
|
|
71
|
+
observed_at: input.observed_at
|
|
72
|
+
)
|
|
73
|
+
fail_on_service_failure!(evidence)
|
|
74
|
+
|
|
75
|
+
success(
|
|
76
|
+
payload: {
|
|
77
|
+
games: games,
|
|
78
|
+
teams: teams,
|
|
79
|
+
observation_batch_id: evidence.data[:observation_batch_id],
|
|
80
|
+
historical_change_count: evidence.data[:historical_change_count]
|
|
81
|
+
},
|
|
82
|
+
http_status: :ok
|
|
83
|
+
)
|
|
84
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
85
|
+
fail_transaction!(
|
|
86
|
+
failure(
|
|
87
|
+
errors: [
|
|
88
|
+
Jumbotron::Errors::Canonical::MutationFailedError.new(details: { message: e.message })
|
|
89
|
+
],
|
|
90
|
+
http_status: :unprocessable_entity,
|
|
91
|
+
meta: { failure_class: "mutation" }
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def sync_one_game!(game_input, league, observed_at, change_set)
|
|
97
|
+
season_result = Services::Canonical::EnsureSeason.call(
|
|
98
|
+
league: league,
|
|
99
|
+
year: game_input.season_year
|
|
100
|
+
)
|
|
101
|
+
fail_on_service_failure!(season_result)
|
|
102
|
+
|
|
103
|
+
phase_result = Services::Canonical::EnsureSeasonPhase.call(
|
|
104
|
+
season: season_result.data[:season],
|
|
105
|
+
phase_key: game_input.season_phase_key
|
|
106
|
+
)
|
|
107
|
+
fail_on_service_failure!(phase_result)
|
|
108
|
+
|
|
109
|
+
group_result = Services::Canonical::EnsureScheduleGroup.call(
|
|
110
|
+
season: season_result.data[:season],
|
|
111
|
+
season_phase: phase_result.data[:season_phase],
|
|
112
|
+
schedule_group_input: game_input.schedule_group
|
|
113
|
+
)
|
|
114
|
+
fail_on_service_failure!(group_result)
|
|
115
|
+
|
|
116
|
+
venue_result = Services::Canonical::UpsertVenue.call(
|
|
117
|
+
venue_input: game_input.venue,
|
|
118
|
+
observed_at: observed_at,
|
|
119
|
+
change_set: change_set
|
|
120
|
+
)
|
|
121
|
+
fail_on_service_failure!(venue_result)
|
|
122
|
+
|
|
123
|
+
game_result = Services::Canonical::UpsertGame.call(
|
|
124
|
+
game_input: game_input,
|
|
125
|
+
league: league,
|
|
126
|
+
season: season_result.data[:season],
|
|
127
|
+
season_phase: phase_result.data[:season_phase],
|
|
128
|
+
schedule_group: group_result.data[:schedule_group],
|
|
129
|
+
venue: venue_result.data[:venue],
|
|
130
|
+
observed_at: observed_at,
|
|
131
|
+
change_set: change_set
|
|
132
|
+
)
|
|
133
|
+
fail_on_service_failure!(game_result)
|
|
134
|
+
|
|
135
|
+
game = game_result.data[:game]
|
|
136
|
+
|
|
137
|
+
attach = Services::Canonical::AttachProviderIdentities.call(
|
|
138
|
+
target: game,
|
|
139
|
+
refs: game_input.provider_identities
|
|
140
|
+
)
|
|
141
|
+
fail_on_service_failure!(attach)
|
|
142
|
+
|
|
143
|
+
game_input.participants.each do |participant_input|
|
|
144
|
+
gp_result = Services::Canonical::UpsertGameParticipant.call(
|
|
145
|
+
game: game,
|
|
146
|
+
participant_input: participant_input,
|
|
147
|
+
observed_at: observed_at,
|
|
148
|
+
change_set: change_set
|
|
149
|
+
)
|
|
150
|
+
fail_on_service_failure!(gp_result)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
game
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def fail_on_service_failure!(service_result)
|
|
157
|
+
return if service_result.success?
|
|
158
|
+
|
|
159
|
+
fail_transaction!(map_service_failure(service_result, failure_class: "mutation"))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def map_execute_failure(result)
|
|
163
|
+
error = Array(result.errors).first
|
|
164
|
+
code = error.respond_to?(:code) ? error.code : "normalization_failed"
|
|
165
|
+
failure(
|
|
166
|
+
errors: [{ code: code, message: error.respond_to?(:message) ? error.message : error.to_s }],
|
|
167
|
+
http_status: code == "acquisition_failed" ? :bad_gateway : :unprocessable_entity,
|
|
168
|
+
meta: { failure_class: code == "acquisition_failed" ? "acquisition" : "normalization" }
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def map_service_failure(result, failure_class:)
|
|
173
|
+
error = Array(result.errors).first
|
|
174
|
+
code = error.respond_to?(:code) ? error.code : "#{failure_class}_failed"
|
|
175
|
+
failure(
|
|
176
|
+
errors: [{ code: code, message: error.respond_to?(:message) ? error.message : error.to_s }],
|
|
177
|
+
http_status: :unprocessable_entity,
|
|
178
|
+
meta: { failure_class: failure_class }
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateJumbotronFoundationalCatalog < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_sports
|
|
6
|
+
create_leagues
|
|
7
|
+
create_seasons
|
|
8
|
+
create_season_phases
|
|
9
|
+
create_provider_identities
|
|
10
|
+
index_provider_identities
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def create_sports
|
|
16
|
+
create_table :jumbotron_sports do |t|
|
|
17
|
+
t.string :name, null: false
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_leagues
|
|
23
|
+
create_table :jumbotron_leagues do |t|
|
|
24
|
+
t.references :sport, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_sports }
|
|
25
|
+
t.string :name, null: false
|
|
26
|
+
t.timestamps
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_seasons
|
|
31
|
+
create_table :jumbotron_seasons do |t|
|
|
32
|
+
t.references :league, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_leagues }
|
|
33
|
+
t.string :name, null: false
|
|
34
|
+
t.date :starts_on
|
|
35
|
+
t.date :ends_on
|
|
36
|
+
t.timestamps
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_season_phases
|
|
41
|
+
create_table :jumbotron_season_phases do |t|
|
|
42
|
+
t.references :season, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_seasons }
|
|
43
|
+
t.string :name, null: false
|
|
44
|
+
t.timestamps
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def create_provider_identities
|
|
49
|
+
create_table :jumbotron_provider_identities do |t|
|
|
50
|
+
t.string :provider, null: false, limit: 64
|
|
51
|
+
t.string :object_namespace, null: false, limit: 64
|
|
52
|
+
t.string :provider_id, null: false, limit: 191
|
|
53
|
+
t.string :target_type, null: false
|
|
54
|
+
t.bigint :target_id, null: false
|
|
55
|
+
t.timestamps
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def index_provider_identities
|
|
60
|
+
add_index :jumbotron_provider_identities,
|
|
61
|
+
%i[provider object_namespace provider_id],
|
|
62
|
+
unique: true,
|
|
63
|
+
name: "idx_jumbotron_pi_identity"
|
|
64
|
+
add_index :jumbotron_provider_identities,
|
|
65
|
+
%i[target_type target_id],
|
|
66
|
+
name: "idx_jumbotron_pi_target"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateJumbotronGameGraph < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_teams
|
|
6
|
+
create_venues
|
|
7
|
+
create_bookmakers
|
|
8
|
+
create_games
|
|
9
|
+
create_game_participants
|
|
10
|
+
index_game_participants
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def create_teams
|
|
16
|
+
create_table :jumbotron_teams do |t|
|
|
17
|
+
t.string :name, null: false
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_venues
|
|
23
|
+
create_table :jumbotron_venues do |t|
|
|
24
|
+
t.string :name, null: false
|
|
25
|
+
t.timestamps
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_bookmakers
|
|
30
|
+
create_table :jumbotron_bookmakers do |t|
|
|
31
|
+
t.string :name, null: false
|
|
32
|
+
t.timestamps
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create_games
|
|
37
|
+
create_table :jumbotron_games do |t|
|
|
38
|
+
add_game_references(t)
|
|
39
|
+
t.datetime :scheduled_at
|
|
40
|
+
t.string :lifecycle, null: false, default: "scheduled"
|
|
41
|
+
t.boolean :neutral_site, null: false, default: false
|
|
42
|
+
t.timestamps
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_game_references(table)
|
|
47
|
+
table.references :league, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_leagues }
|
|
48
|
+
table.references :season, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_seasons }
|
|
49
|
+
table.references :season_phase, type: :bigint, foreign_key: { to_table: :jumbotron_season_phases }
|
|
50
|
+
table.references :venue, type: :bigint, foreign_key: { to_table: :jumbotron_venues }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_game_participants
|
|
54
|
+
create_table :jumbotron_game_participants do |t|
|
|
55
|
+
t.references :game, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_games }
|
|
56
|
+
t.references :team, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_teams }
|
|
57
|
+
t.string :role
|
|
58
|
+
t.integer :score
|
|
59
|
+
t.string :result
|
|
60
|
+
t.timestamps
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def index_game_participants
|
|
65
|
+
add_index :jumbotron_game_participants,
|
|
66
|
+
%i[game_id team_id],
|
|
67
|
+
unique: true,
|
|
68
|
+
name: "idx_jumbotron_gp_game_team"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddFreshnessToJumbotronProviderMutatedEntities < ActiveRecord::Migration[7.2]
|
|
4
|
+
TABLES = %w[
|
|
5
|
+
jumbotron_games
|
|
6
|
+
jumbotron_teams
|
|
7
|
+
jumbotron_venues
|
|
8
|
+
jumbotron_game_participants
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
def up
|
|
12
|
+
TABLES.each do |table|
|
|
13
|
+
add_column table, :observed_at, :datetime
|
|
14
|
+
add_column table, :changed_at, :datetime
|
|
15
|
+
execute <<~SQL.squish
|
|
16
|
+
UPDATE #{table}
|
|
17
|
+
SET observed_at = created_at, changed_at = created_at
|
|
18
|
+
WHERE observed_at IS NULL OR changed_at IS NULL
|
|
19
|
+
SQL
|
|
20
|
+
change_column_null table, :observed_at, false
|
|
21
|
+
change_column_null table, :changed_at, false
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def down
|
|
26
|
+
TABLES.each do |table|
|
|
27
|
+
remove_column table, :observed_at
|
|
28
|
+
remove_column table, :changed_at
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateJumbotronObservationEvidence < ActiveRecord::Migration[7.2]
|
|
4
|
+
def change
|
|
5
|
+
create_table :jumbotron_observation_batches do |t|
|
|
6
|
+
t.string :provider, null: false
|
|
7
|
+
t.string :adapter_scope, null: false
|
|
8
|
+
t.datetime :observed_at, null: false
|
|
9
|
+
t.json :metadata
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
add_index :jumbotron_observation_batches,
|
|
13
|
+
%i[provider adapter_scope observed_at],
|
|
14
|
+
name: "idx_jumbotron_obs_batches_scope_time"
|
|
15
|
+
|
|
16
|
+
create_table :jumbotron_historical_changes do |t|
|
|
17
|
+
t.references :observation_batch,
|
|
18
|
+
null: false,
|
|
19
|
+
foreign_key: { to_table: :jumbotron_observation_batches },
|
|
20
|
+
index: { name: "idx_jumbotron_hist_changes_batch" }
|
|
21
|
+
t.string :subject_type, null: false
|
|
22
|
+
t.bigint :subject_id, null: false
|
|
23
|
+
t.string :attribute_name, null: false
|
|
24
|
+
t.json :previous_value
|
|
25
|
+
t.json :new_value
|
|
26
|
+
t.datetime :observed_at, null: false
|
|
27
|
+
t.string :provider, null: false
|
|
28
|
+
t.timestamps
|
|
29
|
+
end
|
|
30
|
+
add_index :jumbotron_historical_changes,
|
|
31
|
+
%i[subject_type subject_id],
|
|
32
|
+
name: "idx_jumbotron_hist_changes_subject"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateJumbotronScheduleGroups < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :jumbotron_schedule_groups do |t|
|
|
6
|
+
t.references :season, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_seasons }
|
|
7
|
+
t.references :season_phase, type: :bigint, foreign_key: { to_table: :jumbotron_season_phases }
|
|
8
|
+
t.string :kind, null: false
|
|
9
|
+
t.integer :number
|
|
10
|
+
t.string :name, null: false
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :jumbotron_schedule_groups,
|
|
15
|
+
%i[season_id season_phase_id kind name],
|
|
16
|
+
unique: true,
|
|
17
|
+
name: "idx_jumbotron_sg_identity"
|
|
18
|
+
|
|
19
|
+
add_reference :jumbotron_games,
|
|
20
|
+
:schedule_group,
|
|
21
|
+
type: :bigint,
|
|
22
|
+
foreign_key: { to_table: :jumbotron_schedule_groups }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateJumbotronLineObservations < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
add_column :jumbotron_bookmakers, :observed_at, :datetime
|
|
6
|
+
add_column :jumbotron_bookmakers, :changed_at, :datetime
|
|
7
|
+
reversible do |dir|
|
|
8
|
+
dir.up do
|
|
9
|
+
execute <<~SQL.squish
|
|
10
|
+
UPDATE jumbotron_bookmakers
|
|
11
|
+
SET observed_at = created_at, changed_at = created_at
|
|
12
|
+
WHERE observed_at IS NULL OR changed_at IS NULL
|
|
13
|
+
SQL
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
change_column_null :jumbotron_bookmakers, :observed_at, false
|
|
17
|
+
change_column_null :jumbotron_bookmakers, :changed_at, false
|
|
18
|
+
|
|
19
|
+
create_table :jumbotron_line_observations do |t|
|
|
20
|
+
t.references :game, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_games }
|
|
21
|
+
t.references :bookmaker, null: false, type: :bigint, foreign_key: { to_table: :jumbotron_bookmakers }
|
|
22
|
+
t.references :observation_batch, null: false, type: :bigint,
|
|
23
|
+
foreign_key: { to_table: :jumbotron_observation_batches }
|
|
24
|
+
t.string :market, null: false, limit: 32
|
|
25
|
+
t.string :outcome, null: false, limit: 32
|
|
26
|
+
t.string :source, null: false, limit: 32
|
|
27
|
+
t.decimal :line_value, precision: 8, scale: 3
|
|
28
|
+
t.integer :price_american
|
|
29
|
+
t.datetime :observed_at, null: false
|
|
30
|
+
t.datetime :changed_at, null: false
|
|
31
|
+
t.timestamps
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
add_index :jumbotron_line_observations,
|
|
35
|
+
%i[game_id bookmaker_id market outcome source observed_at],
|
|
36
|
+
name: "idx_jumbotron_line_obs_latest"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddProgressToJumbotronGames < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
change_table :jumbotron_games do |t|
|
|
6
|
+
t.string :progress_state
|
|
7
|
+
t.string :progress_segment_kind
|
|
8
|
+
t.integer :progress_segment_number
|
|
9
|
+
t.string :progress_clock_mode
|
|
10
|
+
t.integer :progress_clock_seconds
|
|
11
|
+
t.string :progress_clock_display
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
class Client
|
|
5
|
+
def schedule(**)
|
|
6
|
+
Public::TranslateOutcome.call(
|
|
7
|
+
Workflows::ReadScheduleWorkflow.call(**),
|
|
8
|
+
payload_key: :schedule
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def schedule_groups(**)
|
|
13
|
+
Public::TranslateOutcome.call(
|
|
14
|
+
Workflows::ReadScheduleGroupsWorkflow.call(**),
|
|
15
|
+
payload_key: :schedule_groups
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def game(**)
|
|
20
|
+
Public::TranslateOutcome.call(
|
|
21
|
+
Workflows::ReadGameWorkflow.call(**),
|
|
22
|
+
payload_key: :game
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def consensus(**)
|
|
27
|
+
Public::TranslateOutcome.call(
|
|
28
|
+
Workflows::ReadConsensusWorkflow.call(**),
|
|
29
|
+
payload_key: :consensus_lines
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def current_lines(**)
|
|
34
|
+
Public::TranslateOutcome.call(
|
|
35
|
+
Workflows::ReadCurrentLinesWorkflow.call(**),
|
|
36
|
+
payload_key: :current_lines
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Jumbotron
|
|
6
|
+
|
|
7
|
+
# Ensure app/synchronization is on Zeitwerk paths (alongside adapters/clients/providers).
|
|
8
|
+
config.autoload_paths << root.join("app/synchronization")
|
|
9
|
+
config.eager_load_paths << root.join("app/synchronization")
|
|
10
|
+
config.autoload_paths << root.join("app/scheduling")
|
|
11
|
+
config.eager_load_paths << root.join("app/scheduling")
|
|
12
|
+
|
|
13
|
+
config.autoload_paths << root.join("app/boot")
|
|
14
|
+
config.eager_load_paths << root.join("app/boot")
|
|
15
|
+
|
|
16
|
+
config.boot_discovery = true
|
|
17
|
+
|
|
18
|
+
# Reconcile jumbotron:* Sidekiq-Cron jobs on Sidekiq server start only.
|
|
19
|
+
# Do not require Sidekiq; skip when the host has not loaded Sidekiq-Cron.
|
|
20
|
+
config.after_initialize do
|
|
21
|
+
Jumbotron::Boot::EnqueueDiscoveries.call
|
|
22
|
+
|
|
23
|
+
next unless defined?(::Sidekiq) && defined?(::Sidekiq::Cron::Job)
|
|
24
|
+
|
|
25
|
+
::Sidekiq.configure_server do |sidekiq|
|
|
26
|
+
sidekiq.on(:startup) do
|
|
27
|
+
Jumbotron::Scheduling::Materializer.call
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/jumbotron.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :jumbotron do
|
|
4
|
+
namespace :schedules do
|
|
5
|
+
desc "Materialize Jumbotron recurring schedules into the host scheduler"
|
|
6
|
+
task materialize: :environment do
|
|
7
|
+
Jumbotron::Scheduling::Materializer.call(
|
|
8
|
+
solid_queue_path: ENV.fetch("JUMBOTRON_SOLID_QUEUE_RECURRING", nil)
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|