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,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Scheduling
|
|
5
|
+
class Clock
|
|
6
|
+
MINUTE = 17
|
|
7
|
+
HOUR = 6
|
|
8
|
+
WEEKDAY = 3
|
|
9
|
+
WEEKDAY_NAME = "wednesday"
|
|
10
|
+
|
|
11
|
+
def self.clock_time
|
|
12
|
+
"#{HOUR}:#{format('%02d', MINUTE)} am"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.fugit(cadence)
|
|
16
|
+
every = cadence.every
|
|
17
|
+
case canonical_unit(cadence.unit)
|
|
18
|
+
when :second then every == 1 ? "every second" : "every #{every} seconds"
|
|
19
|
+
when :minute then every == 1 ? "every minute" : "every #{every} minutes"
|
|
20
|
+
when :hour then hour_fugit(every)
|
|
21
|
+
when :day then day_fugit(every)
|
|
22
|
+
when :week then "every #{WEEKDAY_NAME} at #{clock_time}"
|
|
23
|
+
else
|
|
24
|
+
raise ArgumentError, "unsupported cadence unit: #{cadence.unit}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.cron(cadence)
|
|
29
|
+
every = cadence.every
|
|
30
|
+
case canonical_unit(cadence.unit)
|
|
31
|
+
when :minute then "*/#{every} * * * *"
|
|
32
|
+
when :hour then hour_cron(every)
|
|
33
|
+
when :day then day_cron(every)
|
|
34
|
+
when :week then "#{MINUTE} #{HOUR} * * #{WEEKDAY}"
|
|
35
|
+
else
|
|
36
|
+
raise ArgumentError, "unsupported cadence unit: #{cadence.unit}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.canonical_unit(unit)
|
|
41
|
+
unit.to_s.sub(/s\z/, "").to_sym
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.hour_fugit(every)
|
|
45
|
+
every == 1 ? "every hour at minute #{MINUTE}" : "every #{every} hours at minute #{MINUTE}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.day_fugit(every)
|
|
49
|
+
every == 1 ? "every day at #{clock_time}" : "every #{every} days at #{clock_time}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.hour_cron(every)
|
|
53
|
+
every == 1 ? "#{MINUTE} * * * *" : "#{MINUTE} */#{every} * * *"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.day_cron(every)
|
|
57
|
+
every == 1 ? "#{MINUTE} #{HOUR} * * *" : "#{MINUTE} #{HOUR} */#{every} * *"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Scheduling
|
|
5
|
+
module DesiredSchedules
|
|
6
|
+
GAME_JOB_CLASS_NAME = "Jumbotron::GameUpdateJob"
|
|
7
|
+
LINE_JOB_CLASS_NAME = "Jumbotron::LineUpdateJob"
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
ensure_adapters_loaded!
|
|
13
|
+
Adapters::Registry.registered.flat_map { |adapter| schedules_for(adapter) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def schedules_for(adapter)
|
|
17
|
+
adapter.policies.values.filter_map { |policy| definition_for(adapter, policy) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def definition_for(adapter, policy)
|
|
21
|
+
return if adapter.discovery_ids.include?(policy.id)
|
|
22
|
+
return if policy.cadence.nil?
|
|
23
|
+
|
|
24
|
+
job_class_name = job_class_name_for(policy)
|
|
25
|
+
return if job_class_name.nil?
|
|
26
|
+
|
|
27
|
+
ScheduleDefinition.new(
|
|
28
|
+
id: Owned.id(adapter.adapter_id, policy.id),
|
|
29
|
+
job_class_name: job_class_name,
|
|
30
|
+
arguments: {
|
|
31
|
+
"adapter_id" => adapter.adapter_id.to_s,
|
|
32
|
+
"policy_id" => policy.id.to_s
|
|
33
|
+
},
|
|
34
|
+
cadence: policy.cadence
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def job_class_name_for(policy)
|
|
39
|
+
if Services::Adapters::ResolveGameUpdatePolicy::UPDATE_POLICY_TYPES.include?(policy.type)
|
|
40
|
+
GAME_JOB_CLASS_NAME
|
|
41
|
+
elsif Services::Adapters::ResolveLineUpdatePolicy::LINE_POLICY_TYPES.include?(policy.type)
|
|
42
|
+
LINE_JOB_CLASS_NAME
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ensure_adapters_loaded!
|
|
47
|
+
Adapters::Registry.ensure_loaded!
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Scheduling
|
|
5
|
+
class UnsupportedIntegrationError < StandardError; end
|
|
6
|
+
|
|
7
|
+
module DetectIntegration
|
|
8
|
+
SUPPORTED = "Solid Queue, Sidekiq + Sidekiq-Cron"
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
name = adapter_name
|
|
14
|
+
return Backends::SolidQueue.new if name == :solid_queue
|
|
15
|
+
return sidekiq_backend! if name == :sidekiq
|
|
16
|
+
|
|
17
|
+
raise_unsupported(name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def adapter_name
|
|
21
|
+
return ActiveJob::Base.queue_adapter_name.to_s.to_sym if ActiveJob::Base.respond_to?(:queue_adapter_name)
|
|
22
|
+
|
|
23
|
+
configured = Rails.application.config.active_job.queue_adapter
|
|
24
|
+
return configured.to_sym if configured.is_a?(Symbol)
|
|
25
|
+
|
|
26
|
+
ActiveJob::Base.queue_adapter.class.name.to_s.demodulize.underscore.sub(/_adapter\z/, "").to_sym
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sidekiq_backend!
|
|
30
|
+
return Backends::SidekiqCron.new if defined?(::Sidekiq::Cron::Job)
|
|
31
|
+
|
|
32
|
+
raise UnsupportedIntegrationError,
|
|
33
|
+
"Jumbotron recurring schedules cannot be materialized for ActiveJob adapter :sidekiq " \
|
|
34
|
+
"because no supported recurring scheduler is present. " \
|
|
35
|
+
"Supported scheduling integrations: #{SUPPORTED}."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def raise_unsupported(name)
|
|
39
|
+
raise UnsupportedIntegrationError,
|
|
40
|
+
"Jumbotron recurring schedules cannot be materialized for ActiveJob adapter :#{name}. " \
|
|
41
|
+
"Supported scheduling integrations: #{SUPPORTED}."
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Scheduling
|
|
5
|
+
class Materializer
|
|
6
|
+
def self.call(schedules: nil, backend: nil, solid_queue_path: nil)
|
|
7
|
+
schedules ||= DesiredSchedules.call
|
|
8
|
+
backend ||= DetectIntegration.call
|
|
9
|
+
backend.materialize(schedules, path: solid_queue_path)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Scheduling
|
|
5
|
+
module Owned
|
|
6
|
+
PREFIX = "jumbotron:"
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def key?(name)
|
|
11
|
+
name.to_s.start_with?(PREFIX)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def id(adapter_id, policy_id)
|
|
15
|
+
"#{PREFIX}#{adapter_id}:#{policy_id}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Serializers
|
|
5
|
+
module Clients
|
|
6
|
+
module Espn
|
|
7
|
+
module CompetitionOdds
|
|
8
|
+
class Get
|
|
9
|
+
def self.call(_input)
|
|
10
|
+
CommandTower::Clients::SerializedRequest.build(query: {})
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Serializers
|
|
5
|
+
module Clients
|
|
6
|
+
module Espn
|
|
7
|
+
module Scoreboard
|
|
8
|
+
class Get
|
|
9
|
+
def self.call(input)
|
|
10
|
+
query = {}
|
|
11
|
+
query["seasontype"] = input.season_type unless input.season_type.nil?
|
|
12
|
+
query["week"] = input.week unless input.week.nil?
|
|
13
|
+
query["dates"] = input.dates unless input.dates.nil?
|
|
14
|
+
|
|
15
|
+
CommandTower::Clients::SerializedRequest.build(query: query)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Serializers
|
|
5
|
+
module Clients
|
|
6
|
+
module Espn
|
|
7
|
+
module Teams
|
|
8
|
+
class Get
|
|
9
|
+
def self.call(input)
|
|
10
|
+
query = {}
|
|
11
|
+
query["limit"] = input.limit unless input.limit.nil?
|
|
12
|
+
|
|
13
|
+
CommandTower::Clients::SerializedRequest.build(query: query)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
class BuildAcquisitionScopes < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :adapter, required: true
|
|
8
|
+
validate :games, required: true
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
seen = {}
|
|
12
|
+
scopes = []
|
|
13
|
+
incomplete = 0
|
|
14
|
+
Array(games).each do |game|
|
|
15
|
+
scope = adapter.acquisition_scope_for(game)
|
|
16
|
+
if scope.nil?
|
|
17
|
+
incomplete += 1
|
|
18
|
+
next
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
key = [
|
|
22
|
+
scope.fetch(:endpoint),
|
|
23
|
+
Synchronization::Lease.normalized_acquisition_parts(scope.fetch(:acquisition))
|
|
24
|
+
]
|
|
25
|
+
next if seen[key]
|
|
26
|
+
|
|
27
|
+
seen[key] = true
|
|
28
|
+
scopes << { endpoint: scope.fetch(:endpoint), acquisition: scope.fetch(:acquisition) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context.scopes = scopes
|
|
32
|
+
context.skipped_incomplete_scope = incomplete
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
class BuildLineAcquisitionScopes < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :adapter, required: true
|
|
8
|
+
validate :games, required: true
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
seen = {}
|
|
12
|
+
scopes = []
|
|
13
|
+
incomplete = 0
|
|
14
|
+
games_by_id = Game.where(id: Array(games).map(&:id)).includes(:provider_identities).index_by(&:id)
|
|
15
|
+
Array(games).each do |game|
|
|
16
|
+
scoped_game = games_by_id[game.id] || game
|
|
17
|
+
scope = adapter.line_acquisition_scope_for(scoped_game)
|
|
18
|
+
if scope.nil?
|
|
19
|
+
incomplete += 1
|
|
20
|
+
next
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
key = [
|
|
24
|
+
scope.fetch(:endpoint),
|
|
25
|
+
Synchronization::Lease.normalized_acquisition_parts(scope.fetch(:acquisition))
|
|
26
|
+
]
|
|
27
|
+
next if seen[key]
|
|
28
|
+
|
|
29
|
+
seen[key] = true
|
|
30
|
+
scopes << { endpoint: scope.fetch(:endpoint), acquisition: scope.fetch(:acquisition) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context.scopes = scopes
|
|
34
|
+
context.skipped_incomplete_scope = incomplete
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
class EnsureLeague < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :adapter, required: true
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
sport = find_or_create_sport
|
|
11
|
+
league = find_or_create_league(sport)
|
|
12
|
+
context.league = league
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def find_or_create_sport
|
|
18
|
+
existing = Jumbotron::Sport.where("LOWER(name) = ?", adapter.sport.downcase).first
|
|
19
|
+
existing || Jumbotron::Sport.create!(name: adapter.sport)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_or_create_league(sport)
|
|
23
|
+
existing = sport.leagues.where("LOWER(name) = ?", adapter.league.downcase).first
|
|
24
|
+
existing || sport.leagues.create!(name: adapter.league)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
class ExecuteEndpoint < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :adapter, required: true
|
|
8
|
+
validate :endpoint, required: true
|
|
9
|
+
validate :observed_at, required: true
|
|
10
|
+
validate :acquisition, is_a: Hash, required: false
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
operation = adapter.operation(endpoint)
|
|
14
|
+
provider_result = operation.acquire(**(acquisition || {}))
|
|
15
|
+
unless provider_result.success?
|
|
16
|
+
fail_acquisition!(provider_result)
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
context.sync_input = operation.transform(provider_result.output, observed_at: observed_at)
|
|
22
|
+
rescue Jumbotron::Adapters::TransformError => e
|
|
23
|
+
context.fail!(
|
|
24
|
+
application_error: Jumbotron::Errors::Adapters::NormalizationFailedError.new(
|
|
25
|
+
details: { message: e.message }
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def fail_acquisition!(provider_result)
|
|
34
|
+
error = provider_result.error
|
|
35
|
+
if error.is_a?(Jumbotron::Providers::Espn::CooldownActiveError)
|
|
36
|
+
context.fail!(application_error: Jumbotron::Errors::Adapters::ProviderCooldownActiveError.new)
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context.fail!(
|
|
41
|
+
application_error: Jumbotron::Errors::Adapters::AcquisitionFailedError.new(
|
|
42
|
+
details: { message: error&.message || "provider acquisition failed" }
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
class ExecuteLineAcquisitionScope < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :adapter, required: true
|
|
8
|
+
validate :acquisition, is_a: Hash, required: true
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
if adapter.provider_cooling_down?
|
|
12
|
+
context.outcome = :cooldown_active
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
lease = Synchronization::Lease.new
|
|
17
|
+
lease_scope = Synchronization::Lease.scope(
|
|
18
|
+
adapter_id: adapter.adapter_id,
|
|
19
|
+
endpoint: :competition_odds,
|
|
20
|
+
acquisition: acquisition
|
|
21
|
+
)
|
|
22
|
+
held = lease.acquire(lease_scope)
|
|
23
|
+
unless held.acquired?
|
|
24
|
+
context.outcome = :contention
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
execute_held
|
|
30
|
+
ensure
|
|
31
|
+
lease.release(lease_scope, token: held.token)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def execute_held
|
|
38
|
+
provider_result = adapter.acquire_line_odds(acquisition)
|
|
39
|
+
unless provider_result.success?
|
|
40
|
+
record_acquisition_failure(provider_result)
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
ingest = adapter.translate_line_odds(
|
|
45
|
+
provider_result.output,
|
|
46
|
+
observed_at: Time.current,
|
|
47
|
+
acquisition: acquisition
|
|
48
|
+
)
|
|
49
|
+
persisted = Canonical::PersistLineObservations.call(ingest: ingest)
|
|
50
|
+
unless persisted.success?
|
|
51
|
+
context.outcome = :failed
|
|
52
|
+
context.failure_class = "mutation"
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context.outcome = :succeeded
|
|
57
|
+
rescue Jumbotron::Adapters::TransformError
|
|
58
|
+
context.outcome = :failed
|
|
59
|
+
context.failure_class = "mutation"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def record_acquisition_failure(provider_result)
|
|
63
|
+
error = provider_result.error
|
|
64
|
+
if error.is_a?(Jumbotron::Providers::Espn::CooldownActiveError)
|
|
65
|
+
context.outcome = :cooldown_active
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context.outcome = :failed
|
|
70
|
+
context.failure_class = "acquisition"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
# Isolates lease + existing SynchronizeAdapterWorkflow.call. The workflow-calls-workflow
|
|
7
|
+
# rule remains deferred; do not invent a SharedSequence here.
|
|
8
|
+
class ExecuteSynchronizationScope < CommandTower::Services::ApplicationService
|
|
9
|
+
validate :adapter, required: true
|
|
10
|
+
validate :league, required: true
|
|
11
|
+
validate :endpoint, required: true
|
|
12
|
+
validate :acquisition, is_a: Hash, required: true
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
lease = Synchronization::Lease.new
|
|
16
|
+
lease_scope = Synchronization::Lease.scope(
|
|
17
|
+
adapter_id: adapter.adapter_id,
|
|
18
|
+
endpoint: endpoint,
|
|
19
|
+
acquisition: acquisition
|
|
20
|
+
)
|
|
21
|
+
held = lease.acquire(lease_scope)
|
|
22
|
+
unless held.acquired?
|
|
23
|
+
context.outcome = :contention
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
record_sync_outcome(
|
|
29
|
+
Workflows::SynchronizeAdapterWorkflow.call(
|
|
30
|
+
adapter: adapter,
|
|
31
|
+
endpoint: endpoint,
|
|
32
|
+
league_id: league.id,
|
|
33
|
+
acquisition: acquisition
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
ensure
|
|
37
|
+
lease.release(lease_scope, token: held.token)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def record_sync_outcome(result)
|
|
44
|
+
if result.success?
|
|
45
|
+
context.outcome = :succeeded
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
code = Array(result.errors).first
|
|
50
|
+
code = code[:code] if code.is_a?(Hash)
|
|
51
|
+
code = code.code if code.respond_to?(:code)
|
|
52
|
+
|
|
53
|
+
if code.to_s == "provider_cooldown_active"
|
|
54
|
+
context.outcome = :cooldown_active
|
|
55
|
+
return
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context.outcome = :failed
|
|
59
|
+
context.failure_class = result.meta[:failure_class]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
class ResolveDiscovery < CommandTower::Services::ApplicationService
|
|
7
|
+
validate :adapter_id, required: true
|
|
8
|
+
validate :discovery_id, required: true
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
Jumbotron::Adapters::Registry.ensure_loaded!
|
|
12
|
+
adapter = Jumbotron::Adapters::Registry.find(adapter_id)
|
|
13
|
+
if adapter.nil?
|
|
14
|
+
context.fail!(
|
|
15
|
+
application_error: Jumbotron::Errors::Adapters::UnknownAdapterError.new(
|
|
16
|
+
details: { adapter_id: adapter_id }
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
unless adapter.registered?
|
|
23
|
+
context.fail!(
|
|
24
|
+
application_error: Jumbotron::Errors::Adapters::NotRegisteredError.new(
|
|
25
|
+
details: { adapter: adapter.to_s }
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
definition = lookup_discovery(adapter)
|
|
32
|
+
return if definition.nil?
|
|
33
|
+
|
|
34
|
+
context.adapter = adapter
|
|
35
|
+
context.definition = definition
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def lookup_discovery(adapter)
|
|
41
|
+
adapter.discovery(discovery_id.to_sym)
|
|
42
|
+
rescue Jumbotron::Adapters::UnknownDiscoveryError
|
|
43
|
+
context.fail!(
|
|
44
|
+
application_error: Jumbotron::Errors::Adapters::UnknownDiscoveryError.new(
|
|
45
|
+
details: { discovery_id: discovery_id }
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Services
|
|
5
|
+
module Adapters
|
|
6
|
+
class ResolveGameUpdatePolicy < CommandTower::Services::ApplicationService
|
|
7
|
+
UPDATE_POLICY_TYPES = %i[
|
|
8
|
+
future_game_update
|
|
9
|
+
live_game_update
|
|
10
|
+
interrupted_game_update
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
validate :adapter_id, required: true
|
|
14
|
+
validate :policy_id, required: true
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
Jumbotron::Adapters::Registry.ensure_loaded!
|
|
18
|
+
adapter = Jumbotron::Adapters::Registry.find(adapter_id)
|
|
19
|
+
if adapter.nil?
|
|
20
|
+
context.fail!(
|
|
21
|
+
application_error: Jumbotron::Errors::Adapters::UnknownAdapterError.new(
|
|
22
|
+
details: { adapter_id: adapter_id }
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
unless adapter.registered?
|
|
29
|
+
context.fail!(
|
|
30
|
+
application_error: Jumbotron::Errors::Adapters::NotRegisteredError.new(
|
|
31
|
+
details: { adapter: adapter.to_s }
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
key = policy_id.to_sym
|
|
38
|
+
if adapter.discovery_ids.include?(key)
|
|
39
|
+
context.fail!(
|
|
40
|
+
application_error: Jumbotron::Errors::Adapters::DiscoveryNotExecutableError.new(
|
|
41
|
+
details: { policy_id: policy_id }
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
policy = lookup_policy(adapter, key)
|
|
48
|
+
return if policy.nil?
|
|
49
|
+
|
|
50
|
+
unless UPDATE_POLICY_TYPES.include?(policy.type)
|
|
51
|
+
context.fail!(application_error: Jumbotron::Errors::Adapters::InvalidPolicyTypeError.new)
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context.adapter = adapter
|
|
56
|
+
context.policy = policy
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def lookup_policy(adapter, key)
|
|
62
|
+
adapter.policy(key)
|
|
63
|
+
rescue Jumbotron::Adapters::UnknownPolicyError
|
|
64
|
+
context.fail!(
|
|
65
|
+
application_error: Jumbotron::Errors::Adapters::UnknownPolicyError.new(
|
|
66
|
+
details: { policy_id: policy_id }
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|