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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0461cd6ea53300eaf51f0283060767da1ee18a74356d20906bb35f34a369061e
|
|
4
|
+
data.tar.gz: 89cf5eec83c074a338b5765225933ae5a3be7480853966239d437ac383f09d0b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: de18ce2882fa4b4a450a6541cb7305456b2e820de3cc71d95dede9fbaec654bef2234c1896aaf16e79cd7fcbc7b11163e2d67634dcf83c8d0a8e9921f2ccbdf9
|
|
7
|
+
data.tar.gz: b54ecfe4c49d62510fc1ec0ea238858487585171b57d5313f3e297415f834511c74c89a50f05049199addd6c9374fbb8633614d4d95cacc4de7b380248cc5454
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 matt-taylor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
class Base
|
|
6
|
+
class << self
|
|
7
|
+
include LineAcquisition
|
|
8
|
+
|
|
9
|
+
def register!
|
|
10
|
+
Registry.register(self)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def registered?
|
|
14
|
+
Registry.registered?(self)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def adapter_id(value = nil)
|
|
18
|
+
if value.nil?
|
|
19
|
+
return @adapter_id if defined?(@adapter_id) && @adapter_id
|
|
20
|
+
|
|
21
|
+
raise ConfigurationError, "#{name} must declare adapter_id"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@adapter_id = value.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def provider(value = nil)
|
|
28
|
+
return @provider || raise(ConfigurationError, "#{name} must declare provider") if value.nil?
|
|
29
|
+
|
|
30
|
+
@provider = value.to_s.downcase
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def sport(value = nil)
|
|
34
|
+
return @sport || raise(ConfigurationError, "#{name} must declare sport") if value.nil?
|
|
35
|
+
|
|
36
|
+
@sport = value.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def league(value = nil)
|
|
40
|
+
return @league || raise(ConfigurationError, "#{name} must declare league") if value.nil?
|
|
41
|
+
|
|
42
|
+
@league = value.to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def endpoint(name, resource:, transformer:)
|
|
46
|
+
endpoints[name.to_sym] = { resource: resource, transformer: transformer }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def endpoints
|
|
50
|
+
@endpoints ||= {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Declare: policy :id, type:, cadence: nil, eligible: nil
|
|
54
|
+
# Lookup: policy(:id)
|
|
55
|
+
def policy(id, type: nil, cadence: nil, eligible: nil)
|
|
56
|
+
key = id.to_sym
|
|
57
|
+
declaring = !type.nil? || !cadence.nil? || !eligible.nil?
|
|
58
|
+
|
|
59
|
+
unless declaring
|
|
60
|
+
return policies.fetch(key) do
|
|
61
|
+
raise UnknownPolicyError, "#{name} has no policy :#{key}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
raise ConfigurationError, "#{name} policy :#{key} must declare type" if type.nil?
|
|
66
|
+
|
|
67
|
+
policies[key] = PolicyDefinition.new(
|
|
68
|
+
id: key,
|
|
69
|
+
type: type.to_sym,
|
|
70
|
+
cadence: cadence,
|
|
71
|
+
eligible: eligible
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def policies
|
|
76
|
+
@policies ||= {}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def policy_ids
|
|
80
|
+
policies.keys
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def discovery(id, endpoint: nil)
|
|
84
|
+
key = id.to_sym
|
|
85
|
+
if endpoint.nil?
|
|
86
|
+
return discoveries.fetch(key) do
|
|
87
|
+
raise UnknownDiscoveryError, "#{name} has no discovery :#{key}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
discoveries[key] = DiscoveryDefinition.new(id: key, endpoint: endpoint.to_sym)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def discoveries
|
|
95
|
+
@discoveries ||= {}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def discovery_ids
|
|
99
|
+
discoveries.keys
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def operation(name)
|
|
103
|
+
Registry.assert_unique_keys!
|
|
104
|
+
definition = endpoints[name.to_sym]
|
|
105
|
+
raise ConfigurationError, "#{self.name} has no endpoint :#{name}" unless definition
|
|
106
|
+
|
|
107
|
+
Operation.new(
|
|
108
|
+
adapter: self,
|
|
109
|
+
resource: definition[:resource],
|
|
110
|
+
transformer: definition[:transformer]
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def registry_key
|
|
115
|
+
[provider, sport, league]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def acquisition_scope_for(_game)
|
|
119
|
+
raise ConfigurationError, "#{name} must implement acquisition_scope_for"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def scopes_for_discovery(_definition)
|
|
123
|
+
raise ConfigurationError, "#{name} must implement scopes_for_discovery"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def provider_cooling_down?
|
|
127
|
+
false
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def provider_retry_after
|
|
131
|
+
0
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
class Cadence
|
|
6
|
+
UNITS = {
|
|
7
|
+
second: 1,
|
|
8
|
+
seconds: 1,
|
|
9
|
+
minute: 60,
|
|
10
|
+
minutes: 60,
|
|
11
|
+
hour: 3600,
|
|
12
|
+
hours: 3600,
|
|
13
|
+
day: 86_400,
|
|
14
|
+
days: 86_400,
|
|
15
|
+
week: 604_800,
|
|
16
|
+
weeks: 604_800
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :every, :unit
|
|
20
|
+
|
|
21
|
+
def initialize(every:, unit:)
|
|
22
|
+
@every = Integer(every)
|
|
23
|
+
@unit = unit.to_sym
|
|
24
|
+
raise ArgumentError, "every must be positive" unless @every.positive?
|
|
25
|
+
raise ArgumentError, "unknown cadence unit: #{unit}" unless UNITS.key?(@unit)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def interval_seconds
|
|
29
|
+
every * UNITS.fetch(unit)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ==(other)
|
|
33
|
+
other.is_a?(self.class) && every == other.every && unit == other.unit
|
|
34
|
+
end
|
|
35
|
+
alias eql? ==
|
|
36
|
+
|
|
37
|
+
def hash
|
|
38
|
+
[self.class, every, unit].hash
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module Espn
|
|
6
|
+
class Nfl < Base
|
|
7
|
+
TERMINAL_LIFECYCLES = %w[completed cancelled].freeze
|
|
8
|
+
INTERRUPTED_LIFECYCLES = %w[postponed suspended].freeze
|
|
9
|
+
|
|
10
|
+
adapter_id "espn_nfl"
|
|
11
|
+
|
|
12
|
+
provider :espn
|
|
13
|
+
sport "football"
|
|
14
|
+
league "nfl"
|
|
15
|
+
|
|
16
|
+
endpoint :teams,
|
|
17
|
+
resource: Providers::Espn::Resources::Teams,
|
|
18
|
+
transformer: NflHelpers::Teams
|
|
19
|
+
|
|
20
|
+
endpoint :scoreboard,
|
|
21
|
+
resource: Providers::Espn::Resources::Scoreboard,
|
|
22
|
+
transformer: NflHelpers::Scoreboard
|
|
23
|
+
|
|
24
|
+
policy :far_future,
|
|
25
|
+
type: :future_game_update,
|
|
26
|
+
cadence: Cadence.new(every: 1, unit: :week),
|
|
27
|
+
eligible: lambda { |game, now:|
|
|
28
|
+
TERMINAL_LIFECYCLES.exclude?(game.lifecycle) &&
|
|
29
|
+
game.scheduled_at > now + 6.weeks
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
policy :near_future,
|
|
33
|
+
type: :future_game_update,
|
|
34
|
+
cadence: Cadence.new(every: 1, unit: :day),
|
|
35
|
+
eligible: lambda { |game, now:|
|
|
36
|
+
return false if TERMINAL_LIFECYCLES.include?(game.lifecycle)
|
|
37
|
+
|
|
38
|
+
scheduled = game.scheduled_at
|
|
39
|
+
scheduled.between?(now + 2.weeks, now + 6.weeks)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
policy :upcoming,
|
|
43
|
+
type: :future_game_update,
|
|
44
|
+
cadence: Cadence.new(every: 1, unit: :hour),
|
|
45
|
+
eligible: lambda { |game, now:|
|
|
46
|
+
return false if TERMINAL_LIFECYCLES.include?(game.lifecycle)
|
|
47
|
+
|
|
48
|
+
scheduled = game.scheduled_at
|
|
49
|
+
scheduled > now && scheduled < now + 2.weeks
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
policy :live,
|
|
53
|
+
type: :live_game_update,
|
|
54
|
+
cadence: Cadence.new(every: 2, unit: :minute),
|
|
55
|
+
eligible: lambda { |game, now:|
|
|
56
|
+
return false if TERMINAL_LIFECYCLES.include?(game.lifecycle)
|
|
57
|
+
return false if INTERRUPTED_LIFECYCLES.include?(game.lifecycle)
|
|
58
|
+
|
|
59
|
+
now >= game.scheduled_at
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
policy :interrupted,
|
|
63
|
+
type: :interrupted_game_update,
|
|
64
|
+
cadence: Cadence.new(every: 1, unit: :hour),
|
|
65
|
+
eligible: lambda { |game, **|
|
|
66
|
+
INTERRUPTED_LIFECYCLES.include?(game.lifecycle)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
policy :far_future_lines,
|
|
70
|
+
type: :future_line_update,
|
|
71
|
+
cadence: Cadence.new(every: 1, unit: :week),
|
|
72
|
+
eligible: lambda { |game, now:|
|
|
73
|
+
TERMINAL_LIFECYCLES.exclude?(game.lifecycle) &&
|
|
74
|
+
game.scheduled_at > now + 6.weeks
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
policy :near_future_lines,
|
|
78
|
+
type: :future_line_update,
|
|
79
|
+
cadence: Cadence.new(every: 1, unit: :day),
|
|
80
|
+
eligible: lambda { |game, now:|
|
|
81
|
+
return false if TERMINAL_LIFECYCLES.include?(game.lifecycle)
|
|
82
|
+
|
|
83
|
+
scheduled = game.scheduled_at
|
|
84
|
+
scheduled.between?(now + 2.weeks, now + 6.weeks)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
policy :upcoming_lines,
|
|
88
|
+
type: :future_line_update,
|
|
89
|
+
cadence: Cadence.new(every: 1, unit: :hour),
|
|
90
|
+
eligible: lambda { |game, now:|
|
|
91
|
+
return false if TERMINAL_LIFECYCLES.include?(game.lifecycle)
|
|
92
|
+
|
|
93
|
+
scheduled = game.scheduled_at
|
|
94
|
+
scheduled > now && scheduled < now + 2.weeks
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
policy :in_progress_lines,
|
|
98
|
+
type: :live_line_update,
|
|
99
|
+
cadence: Cadence.new(every: 1, unit: :hour),
|
|
100
|
+
eligible: lambda { |game, now:|
|
|
101
|
+
return false if TERMINAL_LIFECYCLES.include?(game.lifecycle)
|
|
102
|
+
return false if INTERRUPTED_LIFECYCLES.include?(game.lifecycle)
|
|
103
|
+
|
|
104
|
+
now >= game.scheduled_at
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
policy :interrupted_lines,
|
|
108
|
+
type: :interrupted_line_update,
|
|
109
|
+
cadence: Cadence.new(every: 1, unit: :hour),
|
|
110
|
+
eligible: lambda { |game, **|
|
|
111
|
+
INTERRUPTED_LIFECYCLES.include?(game.lifecycle)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
discovery :full_season, endpoint: :scoreboard
|
|
115
|
+
|
|
116
|
+
register!
|
|
117
|
+
|
|
118
|
+
def self.acquisition_scope_for(game)
|
|
119
|
+
NflHelpers::AcquisitionScope.call(game)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.line_acquisition_scope_for(game)
|
|
123
|
+
NflHelpers::LineAcquisitionScope.call(game)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.acquire_line_odds(acquisition)
|
|
127
|
+
parts = acquisition.respond_to?(:to_h) ? acquisition.to_h : {}
|
|
128
|
+
parts = parts.symbolize_keys
|
|
129
|
+
Providers::Espn::Resources::CompetitionOdds.acquire(
|
|
130
|
+
sport: sport,
|
|
131
|
+
league: league,
|
|
132
|
+
event_id: parts.fetch(:event_id),
|
|
133
|
+
competition_id: parts.fetch(:competition_id)
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.translate_line_odds(odds, observed_at:, acquisition:)
|
|
138
|
+
parts = acquisition.respond_to?(:to_h) ? acquisition.to_h : {}
|
|
139
|
+
parts = parts.symbolize_keys
|
|
140
|
+
NflHelpers::CompetitionOdds.call(
|
|
141
|
+
odds,
|
|
142
|
+
observed_at: observed_at,
|
|
143
|
+
event_id: parts.fetch(:event_id),
|
|
144
|
+
competition_id: parts.fetch(:competition_id)
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.provider_cooling_down?
|
|
149
|
+
Providers::Espn::Cooldown.new.cooling_down?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.provider_retry_after
|
|
153
|
+
Providers::Espn::Cooldown.new.retry_after
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.scopes_for_discovery(definition)
|
|
157
|
+
unless definition.endpoint == :scoreboard
|
|
158
|
+
raise ConfigurationError, "#{name} discovery #{definition.id} uses unsupported endpoint"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
Providers::Espn::Resources::Scoreboard.expand_full_season(
|
|
162
|
+
adapter: self,
|
|
163
|
+
season_year: Time.current.utc.year
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module Espn
|
|
6
|
+
module NflHelpers
|
|
7
|
+
module AcquisitionScope
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(game)
|
|
11
|
+
group = game.schedule_group
|
|
12
|
+
return if group.nil? || group.kind != "week" || group.number.nil?
|
|
13
|
+
|
|
14
|
+
year = Integer(game.season.name)
|
|
15
|
+
season_type = SeasonPhase.espn_season_type(game.season_phase&.name)
|
|
16
|
+
return if season_type.nil?
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
endpoint: :scoreboard,
|
|
20
|
+
acquisition: { dates: year, season_type: season_type, week: group.number }
|
|
21
|
+
}
|
|
22
|
+
rescue ArgumentError, TypeError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Jumbotron
|
|
6
|
+
module Adapters
|
|
7
|
+
module Espn
|
|
8
|
+
module NflHelpers
|
|
9
|
+
class CompetitionOdds
|
|
10
|
+
SOURCES = {
|
|
11
|
+
"observed" => :current,
|
|
12
|
+
"provider_open" => :open,
|
|
13
|
+
"provider_close" => :close
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def self.call(odds, observed_at:, event_id:, competition_id:)
|
|
17
|
+
Canonical::LineIngestInput.new(
|
|
18
|
+
observed_at: observed_at,
|
|
19
|
+
provider: "espn",
|
|
20
|
+
adapter_scope: "espn_nfl",
|
|
21
|
+
game_identities: game_identities(event_id, competition_id),
|
|
22
|
+
observations: Array(odds.items).flat_map { |item| translate_item(item) }
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.game_identities(event_id, competition_id)
|
|
27
|
+
ids = []
|
|
28
|
+
if event_id.present?
|
|
29
|
+
ids << Canonical::ProviderIdentityRef.new(provider: "espn", namespace: "event", id: event_id.to_s)
|
|
30
|
+
end
|
|
31
|
+
if competition_id.present?
|
|
32
|
+
ids << Canonical::ProviderIdentityRef.new(
|
|
33
|
+
provider: "espn",
|
|
34
|
+
namespace: "competition",
|
|
35
|
+
id: competition_id.to_s
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
raise TransformError, "event/competition provider id required" if ids.empty?
|
|
39
|
+
|
|
40
|
+
ids
|
|
41
|
+
end
|
|
42
|
+
private_class_method :game_identities
|
|
43
|
+
|
|
44
|
+
def self.translate_item(item)
|
|
45
|
+
listed = item.listed_provider
|
|
46
|
+
raise TransformError, "listed provider id required" if listed.nil? || listed.id.blank?
|
|
47
|
+
|
|
48
|
+
identity = Canonical::ProviderIdentityRef.new(
|
|
49
|
+
provider: "espn",
|
|
50
|
+
namespace: "bookmaker",
|
|
51
|
+
id: listed.id.to_s
|
|
52
|
+
)
|
|
53
|
+
name = listed.name.to_s.presence || listed.id.to_s
|
|
54
|
+
|
|
55
|
+
SOURCES.flat_map do |source, window|
|
|
56
|
+
observations_for_window(item, identity, name, source, window)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
private_class_method :translate_item
|
|
60
|
+
|
|
61
|
+
def self.observations_for_window(item, identity, name, source, window)
|
|
62
|
+
[
|
|
63
|
+
*spread_rows(item, identity, name, source, window),
|
|
64
|
+
*total_rows(item, identity, name, source, window),
|
|
65
|
+
*moneyline_rows(item, identity, name, source, window)
|
|
66
|
+
]
|
|
67
|
+
end
|
|
68
|
+
private_class_method :observations_for_window
|
|
69
|
+
|
|
70
|
+
def self.spread_rows(item, identity, name, source, window)
|
|
71
|
+
%i[home away].filter_map do |side|
|
|
72
|
+
quote = team_quote(item, side, window)
|
|
73
|
+
line = parse_line_value(quote&.point_spread&.american)
|
|
74
|
+
price = parse_american(quote&.spread&.american)
|
|
75
|
+
price ||= parse_american(item.public_send(:"#{side}_team_odds")&.spread_odds) if source == "observed"
|
|
76
|
+
next if line.nil? || price.nil?
|
|
77
|
+
|
|
78
|
+
Canonical::LineObservationInput.new(
|
|
79
|
+
bookmaker_identity: identity,
|
|
80
|
+
bookmaker_name: name,
|
|
81
|
+
market: "spread",
|
|
82
|
+
outcome: side.to_s,
|
|
83
|
+
source: source,
|
|
84
|
+
line_value: line,
|
|
85
|
+
price_american: price
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
private_class_method :spread_rows
|
|
90
|
+
|
|
91
|
+
def self.total_rows(item, identity, name, source, window)
|
|
92
|
+
market_window = item.public_send(window)
|
|
93
|
+
threshold = parse_line_value(market_window&.total&.american)
|
|
94
|
+
threshold ||= parse_line_value(item.over_under) if source == "observed"
|
|
95
|
+
return [] if threshold.nil?
|
|
96
|
+
|
|
97
|
+
%i[over under].filter_map do |side|
|
|
98
|
+
price = parse_american(market_window&.public_send(side)&.american)
|
|
99
|
+
price ||= parse_american(item.public_send(:"#{side}_odds")) if source == "observed"
|
|
100
|
+
next if price.nil?
|
|
101
|
+
|
|
102
|
+
Canonical::LineObservationInput.new(
|
|
103
|
+
bookmaker_identity: identity,
|
|
104
|
+
bookmaker_name: name,
|
|
105
|
+
market: "total",
|
|
106
|
+
outcome: side.to_s,
|
|
107
|
+
source: source,
|
|
108
|
+
line_value: threshold,
|
|
109
|
+
price_american: price
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
private_class_method :total_rows
|
|
114
|
+
|
|
115
|
+
def self.moneyline_rows(item, identity, name, source, window)
|
|
116
|
+
rows = %i[home away].filter_map do |side|
|
|
117
|
+
quote = team_quote(item, side, window)
|
|
118
|
+
price = parse_american(quote&.money_line&.american)
|
|
119
|
+
price ||= parse_american(item.public_send(:"#{side}_team_odds")&.money_line) if source == "observed"
|
|
120
|
+
next if price.nil?
|
|
121
|
+
|
|
122
|
+
Canonical::LineObservationInput.new(
|
|
123
|
+
bookmaker_identity: identity,
|
|
124
|
+
bookmaker_name: name,
|
|
125
|
+
market: "moneyline",
|
|
126
|
+
outcome: side.to_s,
|
|
127
|
+
source: source,
|
|
128
|
+
line_value: nil,
|
|
129
|
+
price_american: price
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
draw_price = parse_american(item.public_send(window)&.draw&.american)
|
|
134
|
+
draw_price ||= parse_american(item.draw_odds&.money_line) if source == "observed"
|
|
135
|
+
return rows if draw_price.nil?
|
|
136
|
+
|
|
137
|
+
rows + [
|
|
138
|
+
Canonical::LineObservationInput.new(
|
|
139
|
+
bookmaker_identity: identity,
|
|
140
|
+
bookmaker_name: name,
|
|
141
|
+
market: "moneyline",
|
|
142
|
+
outcome: "draw",
|
|
143
|
+
source: source,
|
|
144
|
+
line_value: nil,
|
|
145
|
+
price_american: draw_price
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
end
|
|
149
|
+
private_class_method :moneyline_rows
|
|
150
|
+
|
|
151
|
+
def self.team_quote(item, side, window)
|
|
152
|
+
odds = item.public_send(:"#{side}_team_odds")
|
|
153
|
+
odds&.public_send(window)
|
|
154
|
+
end
|
|
155
|
+
private_class_method :team_quote
|
|
156
|
+
|
|
157
|
+
def self.parse_american(raw)
|
|
158
|
+
return nil if raw.nil? || raw == ""
|
|
159
|
+
return Integer(raw) if raw.is_a?(Integer)
|
|
160
|
+
|
|
161
|
+
if raw.is_a?(Float) || raw.is_a?(BigDecimal)
|
|
162
|
+
return nil unless raw == raw.to_i
|
|
163
|
+
|
|
164
|
+
return Integer(raw)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
Integer(raw.to_s.delete("+"))
|
|
168
|
+
rescue ArgumentError, TypeError
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
private_class_method :parse_american
|
|
172
|
+
|
|
173
|
+
def self.parse_line_value(raw)
|
|
174
|
+
return nil if raw.nil? || raw == ""
|
|
175
|
+
|
|
176
|
+
BigDecimal(raw.to_s.delete("+"))
|
|
177
|
+
rescue ArgumentError, TypeError
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
private_class_method :parse_line_value
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module Espn
|
|
6
|
+
module NflHelpers
|
|
7
|
+
module Lifecycle
|
|
8
|
+
MAP = {
|
|
9
|
+
"STATUS_SCHEDULED" => "scheduled",
|
|
10
|
+
"STATUS_IN_PROGRESS" => "in_progress",
|
|
11
|
+
"STATUS_FINAL" => "completed",
|
|
12
|
+
"STATUS_POSTPONED" => "postponed",
|
|
13
|
+
"STATUS_CANCELED" => "cancelled",
|
|
14
|
+
"STATUS_CANCELLED" => "cancelled"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def call(type_name)
|
|
20
|
+
key = type_name.to_s
|
|
21
|
+
mapped = MAP[key]
|
|
22
|
+
raise TransformError, "unsupported ESPN status: #{key.inspect}" if mapped.nil?
|
|
23
|
+
|
|
24
|
+
mapped
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module Espn
|
|
6
|
+
module NflHelpers
|
|
7
|
+
module LineAcquisitionScope
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(game)
|
|
11
|
+
identities = Array(game.provider_identities)
|
|
12
|
+
event = identities.find { |identity| identity.object_namespace == "event" }
|
|
13
|
+
competition = identities.find { |identity| identity.object_namespace == "competition" }
|
|
14
|
+
return if event.nil? || event.provider_id.blank?
|
|
15
|
+
|
|
16
|
+
competition_id = competition&.provider_id.presence || event.provider_id
|
|
17
|
+
{
|
|
18
|
+
endpoint: :competition_odds,
|
|
19
|
+
acquisition: { event_id: event.provider_id, competition_id: competition_id }
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|