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,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Public
|
|
6
|
+
class AssemblePublicSchedule < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :request, required: true
|
|
8
|
+
validate :league, required: true
|
|
9
|
+
validate :public_games, required: false
|
|
10
|
+
validate :season, required: false
|
|
11
|
+
validate :season_phase, required: false
|
|
12
|
+
validate :schedule_group, required: false
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
context.schedule = Jumbotron::Public::Schedule.new(
|
|
16
|
+
league: build_league(league),
|
|
17
|
+
season: build_season(season),
|
|
18
|
+
season_phase: build_phase(season_phase),
|
|
19
|
+
group: build_group(schedule_group),
|
|
20
|
+
starts_at: request.starts_at,
|
|
21
|
+
ends_at: request.ends_at,
|
|
22
|
+
games: Array(public_games)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_league(record)
|
|
29
|
+
sport = Jumbotron::Public::Sport.new(id: record.sport.id, name: record.sport.name)
|
|
30
|
+
Jumbotron::Public::League.new(id: record.id, name: record.name, sport: sport)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_season(record)
|
|
34
|
+
return if record.nil?
|
|
35
|
+
|
|
36
|
+
Jumbotron::Public::Season.new(
|
|
37
|
+
id: record.id,
|
|
38
|
+
name: record.name,
|
|
39
|
+
starts_on: record.starts_on,
|
|
40
|
+
ends_on: record.ends_on
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_phase(record)
|
|
45
|
+
return if record.nil?
|
|
46
|
+
|
|
47
|
+
Jumbotron::Public::SeasonPhase.new(id: record.id, name: record.name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_group(record)
|
|
51
|
+
return if record.nil?
|
|
52
|
+
|
|
53
|
+
Jumbotron::Public::ScheduleGroup.new(
|
|
54
|
+
id: record.id,
|
|
55
|
+
kind: record.kind,
|
|
56
|
+
number: record.number,
|
|
57
|
+
name: record.name
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Public
|
|
6
|
+
class DeriveConsensusForGames < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :current_lines, required: false
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
derived = Canonical::DeriveConsensusForGames.call(current_lines: current_lines)
|
|
11
|
+
unless derived.success?
|
|
12
|
+
context.fail!(application_error: derived.errors.first)
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context.consensus_lines = Array(derived.data[:consensus_lines])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Public
|
|
6
|
+
class LoadGamesByIds < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :game_ids, required: false
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
ids = Array(game_ids)
|
|
11
|
+
games = Game.includes(ReadGame::ASSOCIATIONS).where(id: ids).to_a
|
|
12
|
+
by_id = games.index_by(&:id)
|
|
13
|
+
context.games = ids.filter_map { |id| by_id[id] }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Public
|
|
6
|
+
class LoadIncludedLines < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :games, required: false
|
|
8
|
+
validate :includes, required: false
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
context.current_lines = []
|
|
12
|
+
context.consensus_lines = []
|
|
13
|
+
return if Array(games).empty?
|
|
14
|
+
return unless needs_currents? || needs_consensus?
|
|
15
|
+
|
|
16
|
+
resolved = ResolveCurrentLinesForGames.call(games: games)
|
|
17
|
+
unless resolved.success?
|
|
18
|
+
context.fail!(application_error: resolved.errors.first)
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
context.current_lines = Array(resolved.data[:current_lines])
|
|
22
|
+
|
|
23
|
+
return unless needs_consensus?
|
|
24
|
+
|
|
25
|
+
derived = DeriveConsensusForGames.call(current_lines: context.current_lines)
|
|
26
|
+
unless derived.success?
|
|
27
|
+
context.fail!(application_error: derived.errors.first)
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context.consensus_lines = Array(derived.data[:consensus_lines])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def needs_currents?
|
|
37
|
+
Array(includes).include?(:current_lines)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def needs_consensus?
|
|
41
|
+
includes = Array(self.includes)
|
|
42
|
+
includes.include?(:consensus) || includes.include?(:consensus_constituents)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Public
|
|
6
|
+
class MapPublicConsensusLines < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :consensus_lines, required: false
|
|
8
|
+
validate :current_lines, required: false
|
|
9
|
+
validate :include_constituents, required: false
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
context.public_consensus_lines = Array(consensus_lines).map do |line|
|
|
13
|
+
Jumbotron::Public::ConsensusLine.new(
|
|
14
|
+
game_id: line.game_id,
|
|
15
|
+
market: line.market,
|
|
16
|
+
outcome: line.outcome,
|
|
17
|
+
line_value: line.line_value,
|
|
18
|
+
constituent_count: line.constituent_count,
|
|
19
|
+
observed_at: observed_at_for(line),
|
|
20
|
+
constituents: constituents_for(line)
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def observed_at_for(line)
|
|
28
|
+
matching = Array(current_lines).select do |current|
|
|
29
|
+
current.game_id == line.game_id && current.market == line.market && current.outcome == line.outcome
|
|
30
|
+
end
|
|
31
|
+
matching.map(&:observed_at).compact.max
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def constituents_for(line)
|
|
35
|
+
return [] unless include_constituents
|
|
36
|
+
|
|
37
|
+
Array(line.constituents).map do |constituent|
|
|
38
|
+
Jumbotron::Public::ConsensusConstituent.new(bookmaker_id: constituent.bookmaker_id)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Public
|
|
6
|
+
class MapPublicCurrentLines < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :current_lines, required: false
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
context.public_current_lines = Array(current_lines).map do |line|
|
|
11
|
+
Jumbotron::Public::CurrentLine.new(
|
|
12
|
+
game_id: line.game_id,
|
|
13
|
+
bookmaker_id: line.bookmaker_id,
|
|
14
|
+
bookmaker_name: line.bookmaker_name,
|
|
15
|
+
market: line.market,
|
|
16
|
+
outcome: line.outcome,
|
|
17
|
+
line_value: line.line_value,
|
|
18
|
+
price_american: line.price_american,
|
|
19
|
+
observed_at: line.observed_at,
|
|
20
|
+
changed_at: line.changed_at
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Public
|
|
6
|
+
class ReadGame < CommandTower::Services::ApplicationService
|
|
7
|
+
ASSOCIATIONS = [
|
|
8
|
+
:venue,
|
|
9
|
+
:season_phase,
|
|
10
|
+
:schedule_group,
|
|
11
|
+
{ league: :sport, season: :league, game_participants: :team }
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
validate :game_id, required: true
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
game = Game.includes(ASSOCIATIONS).find_by(id: game_id)
|
|
18
|
+
if game.nil?
|
|
19
|
+
context.fail!(
|
|
20
|
+
application_error: Jumbotron::Errors::Public::NotFoundError.new(
|
|
21
|
+
details: { code: "game_not_found", message: "game was not found" }
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context.game = game
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Public
|
|
6
|
+
class ReadSchedule < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :request, required: true
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
league = resolve_league
|
|
11
|
+
return if context.failure?
|
|
12
|
+
|
|
13
|
+
if league.nil?
|
|
14
|
+
not_found("league_not_found", "league was not found")
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
season = resolve_season(league)
|
|
19
|
+
return if context.failure?
|
|
20
|
+
|
|
21
|
+
phase = resolve_phase(season)
|
|
22
|
+
return if context.failure?
|
|
23
|
+
|
|
24
|
+
group = resolve_group(season, phase)
|
|
25
|
+
return if context.failure?
|
|
26
|
+
|
|
27
|
+
games = load_games(league, season, phase, group)
|
|
28
|
+
context.league = league
|
|
29
|
+
context.season = season
|
|
30
|
+
context.season_phase = phase
|
|
31
|
+
context.schedule_group = group
|
|
32
|
+
context.games = games
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def resolve_league
|
|
38
|
+
return League.includes(:sport).find_by(id: request.league_id) if request.league_id.present?
|
|
39
|
+
|
|
40
|
+
sport = Sport.where("LOWER(name) = ?", request.sport.to_s.downcase).first
|
|
41
|
+
return if sport.nil?
|
|
42
|
+
|
|
43
|
+
sport.leagues.where("LOWER(name) = ?", request.league.to_s.downcase).first
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def resolve_season(league)
|
|
47
|
+
return if request.season_id.blank? && request.season.blank?
|
|
48
|
+
|
|
49
|
+
season =
|
|
50
|
+
if request.season_id.present?
|
|
51
|
+
Season.find_by(id: request.season_id, league_id: league.id)
|
|
52
|
+
else
|
|
53
|
+
league.seasons.where("LOWER(name) = ?", request.season.to_s.downcase).first
|
|
54
|
+
end
|
|
55
|
+
not_found("season_not_found", "season was not found") if season.nil?
|
|
56
|
+
season
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_phase(season)
|
|
60
|
+
return if request.season_phase.blank?
|
|
61
|
+
|
|
62
|
+
if season.nil?
|
|
63
|
+
not_found("season_not_found", "season was not found")
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
phase = season.season_phases.where("LOWER(name) = ?", request.season_phase.to_s.downcase).first
|
|
68
|
+
not_found("season_phase_not_found", "season phase was not found") if phase.nil?
|
|
69
|
+
phase
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def resolve_group(season, phase)
|
|
73
|
+
return if request.schedule_group_id.blank? && request.group.blank?
|
|
74
|
+
|
|
75
|
+
group =
|
|
76
|
+
if request.schedule_group_id.present?
|
|
77
|
+
ScheduleGroup.find_by(id: request.schedule_group_id)
|
|
78
|
+
else
|
|
79
|
+
if season.nil? || phase.nil?
|
|
80
|
+
not_found("schedule_group_not_found", "schedule group was not found")
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
ScheduleGroup.find_by(
|
|
84
|
+
season_id: season.id,
|
|
85
|
+
season_phase_id: phase.id,
|
|
86
|
+
kind: request.group[:kind],
|
|
87
|
+
number: request.group[:number]
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
not_found("schedule_group_not_found", "schedule group was not found") if group.nil?
|
|
91
|
+
group
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def load_games(league, season, phase, group)
|
|
95
|
+
relation = Game.includes(ReadGame::ASSOCIATIONS).where(league_id: league.id)
|
|
96
|
+
relation = relation.where(season_id: season.id) if season
|
|
97
|
+
relation = relation.where(season_phase_id: phase.id) if phase
|
|
98
|
+
if group
|
|
99
|
+
relation = relation.where(schedule_group_id: group.id)
|
|
100
|
+
elsif request.starts_at && request.ends_at
|
|
101
|
+
relation = relation.where(scheduled_at: request.starts_at..request.ends_at)
|
|
102
|
+
end
|
|
103
|
+
relation.order(:scheduled_at, :id).to_a
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def not_found(code, message)
|
|
107
|
+
context.fail!(
|
|
108
|
+
application_error: Jumbotron::Errors::Public::NotFoundError.new(
|
|
109
|
+
details: { code: code, message: message }
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Public
|
|
6
|
+
# Lists observed ScheduleGroups for a resolved SeasonPhase.
|
|
7
|
+
# Completeness is always :incomplete until a dedicated authoritative catalog exists.
|
|
8
|
+
class ReadScheduleGroups < CommandTower::Services::ApplicationService
|
|
9
|
+
validate :request, required: true
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
league = resolve_league
|
|
13
|
+
return if context.failure?
|
|
14
|
+
|
|
15
|
+
if league.nil?
|
|
16
|
+
not_found("league_not_found", "league was not found")
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
season = resolve_season(league)
|
|
21
|
+
return if context.failure?
|
|
22
|
+
return if season.nil?
|
|
23
|
+
|
|
24
|
+
phase = resolve_phase(season)
|
|
25
|
+
return if context.failure?
|
|
26
|
+
return if phase.nil?
|
|
27
|
+
|
|
28
|
+
records = load_groups(season, phase)
|
|
29
|
+
groups = records.map { |record| build_group(record) }
|
|
30
|
+
|
|
31
|
+
context.schedule_groups = Jumbotron::Public::ScheduleGroupEnumeration.new(
|
|
32
|
+
groups: groups,
|
|
33
|
+
completeness: :incomplete
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def resolve_league
|
|
40
|
+
return League.includes(:sport).find_by(id: request.league_id) if request.league_id.present?
|
|
41
|
+
|
|
42
|
+
sport = Sport.where("LOWER(name) = ?", request.sport.to_s.downcase).first
|
|
43
|
+
return if sport.nil?
|
|
44
|
+
|
|
45
|
+
sport.leagues.where("LOWER(name) = ?", request.league.to_s.downcase).first
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def resolve_season(league)
|
|
49
|
+
season =
|
|
50
|
+
if request.season_id.present?
|
|
51
|
+
Season.find_by(id: request.season_id, league_id: league.id)
|
|
52
|
+
else
|
|
53
|
+
league.seasons.where("LOWER(name) = ?", request.season.to_s.downcase).first
|
|
54
|
+
end
|
|
55
|
+
not_found("season_not_found", "season was not found") if season.nil?
|
|
56
|
+
season
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_phase(season)
|
|
60
|
+
phase = season.season_phases.where("LOWER(name) = ?", request.season_phase.to_s.downcase).first
|
|
61
|
+
not_found("season_phase_not_found", "season phase was not found") if phase.nil?
|
|
62
|
+
phase
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def load_groups(season, phase)
|
|
66
|
+
relation = ScheduleGroup.where(season_id: season.id, season_phase_id: phase.id)
|
|
67
|
+
relation = relation.where(kind: request.kind) if request.kind.present?
|
|
68
|
+
relation.order(Arel.sql("number IS NULL, number ASC, name ASC, id ASC")).to_a
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_group(record)
|
|
72
|
+
Jumbotron::Public::ScheduleGroup.new(
|
|
73
|
+
id: record.id,
|
|
74
|
+
kind: record.kind,
|
|
75
|
+
number: record.number,
|
|
76
|
+
name: record.name
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def not_found(code, message)
|
|
81
|
+
context.fail!(
|
|
82
|
+
application_error: Jumbotron::Errors::Public::NotFoundError.new(
|
|
83
|
+
details: { code: code, message: message }
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Public
|
|
6
|
+
class ResolveCurrentLinesForGames < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :games, required: false
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
resolved = Canonical::ResolveCurrentLinesForGames.call(game_ids: Array(games).map(&:id))
|
|
11
|
+
unless resolved.success?
|
|
12
|
+
context.fail!(application_error: resolved.errors.first)
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context.current_lines = Array(resolved.data[:current_lines])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Jumbotron
|
|
6
|
+
module Synchronization
|
|
7
|
+
class UnsupportedStoreError < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Narrow single-writer lease. Contract is backend-neutral; atomic acquire/release
|
|
10
|
+
# require a capable cache/store (MemoryStore or RedisCacheStore). NullStore is rejected.
|
|
11
|
+
class Lease
|
|
12
|
+
DEFAULT_TTL_SECONDS = 120
|
|
13
|
+
KEY_PREFIX = "jumbotron:synchronization:lease"
|
|
14
|
+
|
|
15
|
+
COMPARE_AND_DELETE_LUA = <<~LUA
|
|
16
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
17
|
+
return redis.call("del", KEYS[1])
|
|
18
|
+
else
|
|
19
|
+
return 0
|
|
20
|
+
end
|
|
21
|
+
LUA
|
|
22
|
+
|
|
23
|
+
Result = Data.define(:acquired, :token, :scope) do
|
|
24
|
+
def acquired?
|
|
25
|
+
acquired
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.scope(adapter_id:, endpoint:, acquisition:)
|
|
30
|
+
parts = [
|
|
31
|
+
adapter_id.to_s,
|
|
32
|
+
endpoint.to_s,
|
|
33
|
+
*normalized_acquisition_parts(acquisition)
|
|
34
|
+
]
|
|
35
|
+
parts.join(":")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.normalized_acquisition_parts(acquisition)
|
|
39
|
+
hash = acquisition.respond_to?(:to_h) ? acquisition.to_h : {}
|
|
40
|
+
hash = hash.transform_keys(&:to_s)
|
|
41
|
+
hash.keys.sort.map { |key| hash[key].to_s }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(store: Rails.cache, ttl: DEFAULT_TTL_SECONDS)
|
|
45
|
+
@store = store
|
|
46
|
+
@ttl = Integer(ttl)
|
|
47
|
+
assert_capable_store!
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def acquire(scope)
|
|
51
|
+
assert_capable_store!
|
|
52
|
+
token = SecureRandom.uuid
|
|
53
|
+
key = cache_key(scope)
|
|
54
|
+
written = store.write(
|
|
55
|
+
key,
|
|
56
|
+
token,
|
|
57
|
+
unless_exist: true,
|
|
58
|
+
expires_in: ttl,
|
|
59
|
+
raw: true
|
|
60
|
+
)
|
|
61
|
+
if written
|
|
62
|
+
Result.new(acquired: true, token: token, scope: scope)
|
|
63
|
+
else
|
|
64
|
+
Result.new(acquired: false, token: nil, scope: scope)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def release(scope, token:)
|
|
69
|
+
assert_capable_store!
|
|
70
|
+
return false if token.nil?
|
|
71
|
+
|
|
72
|
+
compare_and_delete(cache_key(scope), token.to_s)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
attr_reader :store, :ttl
|
|
78
|
+
|
|
79
|
+
def cache_key(scope)
|
|
80
|
+
"#{KEY_PREFIX}:#{scope}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def assert_capable_store!
|
|
84
|
+
case store
|
|
85
|
+
when ActiveSupport::Cache::NullStore
|
|
86
|
+
raise UnsupportedStoreError,
|
|
87
|
+
"ActiveSupport::Cache::NullStore cannot satisfy Synchronization::Lease"
|
|
88
|
+
when ActiveSupport::Cache::MemoryStore,
|
|
89
|
+
ActiveSupport::Cache::RedisCacheStore
|
|
90
|
+
nil
|
|
91
|
+
else
|
|
92
|
+
raise UnsupportedStoreError,
|
|
93
|
+
"#{store.class} cannot satisfy Synchronization::Lease atomic acquire/release"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def compare_and_delete(key, token)
|
|
98
|
+
case store
|
|
99
|
+
when ActiveSupport::Cache::MemoryStore
|
|
100
|
+
memory_compare_and_delete(key, token)
|
|
101
|
+
when ActiveSupport::Cache::RedisCacheStore
|
|
102
|
+
redis_compare_and_delete(key, token)
|
|
103
|
+
else
|
|
104
|
+
raise UnsupportedStoreError, "#{store.class} cannot ownership-safe release"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def memory_compare_and_delete(key, token)
|
|
109
|
+
store.send(:synchronize) do
|
|
110
|
+
current = store.read(key, raw: true)
|
|
111
|
+
next false unless current == token
|
|
112
|
+
|
|
113
|
+
store.delete(key)
|
|
114
|
+
true
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def redis_compare_and_delete(key, token)
|
|
119
|
+
normalized = store.send(:normalize_key, key, store.options)
|
|
120
|
+
store.redis.then do |redis|
|
|
121
|
+
deleted = redis.call("eval", COMPARE_AND_DELETE_LUA, 1, normalized, token)
|
|
122
|
+
deleted.to_i.positive?
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|