its-showtime 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/LICENSE +21 -0
- data/README.md +179 -0
- data/bin/showtime +47 -0
- data/lib/showtime/app.rb +399 -0
- data/lib/showtime/charts.rb +229 -0
- data/lib/showtime/component_registry.rb +38 -0
- data/lib/showtime/components/Components.md +309 -0
- data/lib/showtime/components/alerts.rb +83 -0
- data/lib/showtime/components/base.rb +63 -0
- data/lib/showtime/components/charts.rb +119 -0
- data/lib/showtime/components/data.rb +328 -0
- data/lib/showtime/components/inputs.rb +390 -0
- data/lib/showtime/components/layout.rb +135 -0
- data/lib/showtime/components/media.rb +73 -0
- data/lib/showtime/components/sidebar.rb +130 -0
- data/lib/showtime/components/text.rb +156 -0
- data/lib/showtime/components.rb +18 -0
- data/lib/showtime/compute_tracker.rb +21 -0
- data/lib/showtime/helpers.rb +53 -0
- data/lib/showtime/logger.rb +143 -0
- data/lib/showtime/public/.vite/manifest.json +34 -0
- data/lib/showtime/public/assets/antd-3aDVoXqG.js +447 -0
- data/lib/showtime/public/assets/charts-iowb_sWQ.js +3858 -0
- data/lib/showtime/public/assets/index-B2b3lWS5.js +43 -0
- data/lib/showtime/public/assets/index-M6NVamDM.css +1 -0
- data/lib/showtime/public/assets/react-BE6xecJX.js +32 -0
- data/lib/showtime/public/index.html +19 -0
- data/lib/showtime/public/letter.png +0 -0
- data/lib/showtime/public/logo.png +0 -0
- data/lib/showtime/release.rb +108 -0
- data/lib/showtime/session.rb +131 -0
- data/lib/showtime/version.rb +3 -0
- data/lib/showtime/views/index.erb +32 -0
- data/lib/showtime.rb +157 -0
- metadata +300 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module Showtime
|
|
2
|
+
module Release
|
|
3
|
+
VERSION_REGEX = /\A\d+\.\d+\.\d+\z/
|
|
4
|
+
RELEASE_BRANCH_REGEX = /\Arelease\/\d+\.\d+\.(x|\d+)\z/
|
|
5
|
+
TAG_REGEX = /\Av\d+\.\d+\.\d+\z/
|
|
6
|
+
DATE_REGEX = /\A\d{4}-\d{2}-\d{2}\z/
|
|
7
|
+
|
|
8
|
+
def self.current_version(version_path: default_version_path)
|
|
9
|
+
contents = File.read(version_path)
|
|
10
|
+
match = contents.match(/VERSION\s*=\s*"([^"]+)"/)
|
|
11
|
+
raise ArgumentError, "VERSION not found in #{version_path}" unless match
|
|
12
|
+
|
|
13
|
+
match[1]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.valid_release_branch?(branch_name)
|
|
17
|
+
!!(branch_name =~ RELEASE_BRANCH_REGEX)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.valid_tag_for_version?(tag, version)
|
|
21
|
+
tag == "v#{version}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.valid_version?(version)
|
|
25
|
+
!!(version =~ VERSION_REGEX)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.ensure_version_bumped!(previous_version:, new_version:)
|
|
29
|
+
if compare_versions(new_version, previous_version) <= 0
|
|
30
|
+
raise ArgumentError, "Version must be greater than #{previous_version}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.bump_version!(level, version_path: default_version_path)
|
|
37
|
+
version = current_version(version_path: version_path)
|
|
38
|
+
new_version = bump_version_string(version, level)
|
|
39
|
+
|
|
40
|
+
contents = File.read(version_path)
|
|
41
|
+
updated = contents.sub(/VERSION\s*=\s*"[^"]+"/, "VERSION = \"#{new_version}\"")
|
|
42
|
+
File.write(version_path, updated)
|
|
43
|
+
|
|
44
|
+
new_version
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.update_changelog!(version:, changes:, release_date:, changelog_path: "CHANGELOG.md")
|
|
48
|
+
raise ArgumentError, "Invalid version #{version}" unless version =~ VERSION_REGEX
|
|
49
|
+
raise ArgumentError, "Release date must be YYYY-MM-DD" unless release_date =~ DATE_REGEX
|
|
50
|
+
raise ArgumentError, "Changelog not found at #{changelog_path}" unless File.exist?(changelog_path)
|
|
51
|
+
|
|
52
|
+
content = File.read(changelog_path)
|
|
53
|
+
marker = "## [Unreleased]"
|
|
54
|
+
index = content.index(marker)
|
|
55
|
+
raise ArgumentError, "Missing [Unreleased] section in #{changelog_path}" unless index
|
|
56
|
+
|
|
57
|
+
changes_text = changes.to_s.strip
|
|
58
|
+
changes_text = "#{changes_text}\n" unless changes_text.end_with?("\n")
|
|
59
|
+
release_section = "\n\n## [#{version}] - #{release_date}\n#{changes_text}"
|
|
60
|
+
updated = content.insert(index + marker.length, release_section)
|
|
61
|
+
File.write(changelog_path, updated)
|
|
62
|
+
|
|
63
|
+
updated
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.compare_versions(a, b)
|
|
67
|
+
a_parts = parse_version(a)
|
|
68
|
+
b_parts = parse_version(b)
|
|
69
|
+
a_parts.zip(b_parts).each do |left, right|
|
|
70
|
+
return left <=> right if left != right
|
|
71
|
+
end
|
|
72
|
+
0
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.parse_version(version)
|
|
76
|
+
raise ArgumentError, "Invalid version #{version}" unless version =~ VERSION_REGEX
|
|
77
|
+
|
|
78
|
+
version.split(".").map(&:to_i)
|
|
79
|
+
end
|
|
80
|
+
private_class_method :parse_version
|
|
81
|
+
|
|
82
|
+
def self.bump_version_string(version, level)
|
|
83
|
+
major, minor, patch = parse_version(version)
|
|
84
|
+
|
|
85
|
+
case level.to_s
|
|
86
|
+
when "patch"
|
|
87
|
+
patch += 1
|
|
88
|
+
when "minor"
|
|
89
|
+
minor += 1
|
|
90
|
+
patch = 0
|
|
91
|
+
when "major"
|
|
92
|
+
major += 1
|
|
93
|
+
minor = 0
|
|
94
|
+
patch = 0
|
|
95
|
+
else
|
|
96
|
+
raise ArgumentError, "Unknown bump level #{level}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
[major, minor, patch].join(".")
|
|
100
|
+
end
|
|
101
|
+
private_class_method :bump_version_string
|
|
102
|
+
|
|
103
|
+
def self.default_version_path
|
|
104
|
+
File.expand_path("version.rb", __dir__)
|
|
105
|
+
end
|
|
106
|
+
private_class_method :default_version_path
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'securerandom'
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'showtime/component_registry'
|
|
5
|
+
require 'polars-df'
|
|
6
|
+
|
|
7
|
+
module Showtime
|
|
8
|
+
# Base session class without Singleton
|
|
9
|
+
class BaseSession
|
|
10
|
+
attr_reader :elements, :sidebar_elements, :values, :script_error
|
|
11
|
+
attr_accessor :main_script_path # Added main_script_path
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@script_error = nil
|
|
15
|
+
@main_script_path = nil # Initialize main_script_path
|
|
16
|
+
clear
|
|
17
|
+
reset_counters # Initialize counters
|
|
18
|
+
@component_registry = ComponentRegistry.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :component_registry
|
|
22
|
+
|
|
23
|
+
def clear
|
|
24
|
+
@elements = []
|
|
25
|
+
@sidebar_elements = []
|
|
26
|
+
@values = {}
|
|
27
|
+
@current_container = :main
|
|
28
|
+
@container_stack = []
|
|
29
|
+
@script_error = nil
|
|
30
|
+
reset_counters # Also reset counters on full clear
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reset_counters
|
|
34
|
+
@component_counters = Hash.new(0) # Default new component types to counter 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def next_counter_for(component_type)
|
|
38
|
+
@component_counters[component_type.to_s] += 1
|
|
39
|
+
@component_counters[component_type.to_s]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def clear_elements
|
|
43
|
+
@elements = []
|
|
44
|
+
@sidebar_elements = []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def clear_values
|
|
48
|
+
@values = {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_element(element)
|
|
52
|
+
if @current_container == :sidebar
|
|
53
|
+
@sidebar_elements << element
|
|
54
|
+
elsif @container_stack.empty?
|
|
55
|
+
@elements << element
|
|
56
|
+
else
|
|
57
|
+
# Add to the current container's children
|
|
58
|
+
@container_stack.last.children << element
|
|
59
|
+
end
|
|
60
|
+
element
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def with_container(container)
|
|
64
|
+
old_container = @current_container
|
|
65
|
+
old_stack = @container_stack.dup
|
|
66
|
+
|
|
67
|
+
# Push the container onto the stack
|
|
68
|
+
@container_stack.push(container)
|
|
69
|
+
|
|
70
|
+
yield
|
|
71
|
+
|
|
72
|
+
# Restore the previous state
|
|
73
|
+
@container_stack = old_stack
|
|
74
|
+
@current_container = old_container
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def update_value(key, value)
|
|
78
|
+
old_value = @values[key]
|
|
79
|
+
|
|
80
|
+
# Skip equality comparison for DataFrame objects since they may have different schemas
|
|
81
|
+
unless value.is_a?(Polars::DataFrame) && old_value.is_a?(Polars::DataFrame)
|
|
82
|
+
return value if old_value == value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@values[key] = value
|
|
86
|
+
@component_registry.mark_dirty(key)
|
|
87
|
+
value
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def has_value?(key)
|
|
91
|
+
@values.has_key?(key)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def in_sidebar?
|
|
95
|
+
@current_container == :sidebar
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def get_value(key, default = nil)
|
|
99
|
+
if @values.has_key?(key)
|
|
100
|
+
@values[key]
|
|
101
|
+
else
|
|
102
|
+
default
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def set_script_error(error)
|
|
107
|
+
@script_error = error
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def with_sidebar
|
|
111
|
+
old_container = @current_container
|
|
112
|
+
@current_container = :sidebar
|
|
113
|
+
yield
|
|
114
|
+
@current_container = old_container
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def to_json
|
|
118
|
+
{
|
|
119
|
+
elements: @elements.map(&:to_h),
|
|
120
|
+
sidebar_elements: @sidebar_elements.map(&:to_h),
|
|
121
|
+
values: @values,
|
|
122
|
+
script_error: @script_error
|
|
123
|
+
}.to_json
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Named session class for compatibility with prior references.
|
|
128
|
+
class Session < BaseSession
|
|
129
|
+
# No need to redefine methods, they're inherited from BaseSession.
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Showtime</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="<%= (request&.script_name&.to_s + '/letter.png').squeeze('/') %>" />
|
|
8
|
+
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
window.SHOWTIME_BASE_PATH = "<%= request&.script_name %>";
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<!-- Vite generated assets -->
|
|
18
|
+
<% entry = vite_manifest_entry('index') %>
|
|
19
|
+
<% if entry["css"] %>
|
|
20
|
+
<% entry["css"].each do |css_file| %>
|
|
21
|
+
<link rel="stylesheet" href="<%= (request&.script_name&.to_s + '/' + css_file).squeeze('/') %>">
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<div id="root"></div>
|
|
27
|
+
<script type="module" src="<%= vite_asset_path('react') %>"></script>
|
|
28
|
+
<script type="module" src="<%= vite_asset_path('antd') %>"></script>
|
|
29
|
+
<script type="module" src="<%= vite_asset_path('charts') %>"></script>
|
|
30
|
+
<script type="module" src="<%= vite_asset_path('index') %>"></script>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
data/lib/showtime.rb
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
require 'active_support/all'
|
|
2
|
+
|
|
3
|
+
require "showtime/version"
|
|
4
|
+
require "showtime/logger"
|
|
5
|
+
require "showtime/app"
|
|
6
|
+
require "showtime/components"
|
|
7
|
+
require "showtime/charts"
|
|
8
|
+
require "showtime/session"
|
|
9
|
+
require "showtime/helpers"
|
|
10
|
+
require "showtime/compute_tracker"
|
|
11
|
+
require "showtime/release"
|
|
12
|
+
require "pastel"
|
|
13
|
+
|
|
14
|
+
module Showtime
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
# Configure the framework
|
|
18
|
+
def self.configure
|
|
19
|
+
yield(config) if block_given?
|
|
20
|
+
Logger.configure
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.config
|
|
24
|
+
@config ||= {
|
|
25
|
+
logger: {}
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Add session context management
|
|
30
|
+
def self.with_session(session)
|
|
31
|
+
old_session = Thread.current[:showtime_session]
|
|
32
|
+
Thread.current[:showtime_session] = session
|
|
33
|
+
yield
|
|
34
|
+
ensure
|
|
35
|
+
Thread.current[:showtime_session] = old_session
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.current_session
|
|
39
|
+
session = Thread.current[:showtime_session]
|
|
40
|
+
return session if session
|
|
41
|
+
|
|
42
|
+
raise Error, "No current session. Use Showtime.with_session or run via Showtime::App."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Global state accessor methods
|
|
46
|
+
def self.session
|
|
47
|
+
Showtime.current_session
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Main entry point for the framework
|
|
51
|
+
def self.run(file_path = nil, port: 8501, host: "localhost")
|
|
52
|
+
# Initialize logger if not already configured
|
|
53
|
+
Logger.configure unless Logger.logger
|
|
54
|
+
|
|
55
|
+
# Get the calling script path if not provided
|
|
56
|
+
file_path ||= caller_locations.first&.path
|
|
57
|
+
|
|
58
|
+
# If we're already running (i.e. being called from within a script), just return
|
|
59
|
+
return if @server_started
|
|
60
|
+
|
|
61
|
+
# Try to find the script file
|
|
62
|
+
search_paths = []
|
|
63
|
+
|
|
64
|
+
if file_path
|
|
65
|
+
# If file_path is provided, try it directly and with pwd
|
|
66
|
+
search_paths << file_path
|
|
67
|
+
search_paths << File.expand_path(file_path)
|
|
68
|
+
search_paths << File.join(Dir.pwd, file_path)
|
|
69
|
+
else
|
|
70
|
+
# If no file_path, try to get it from caller_locations
|
|
71
|
+
caller_path = caller_locations.first&.path
|
|
72
|
+
if caller_path && caller_path != "(eval)"
|
|
73
|
+
search_paths << caller_path
|
|
74
|
+
search_paths << File.expand_path(caller_path)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
search_paths.compact!
|
|
79
|
+
search_paths.uniq!
|
|
80
|
+
|
|
81
|
+
script_path = search_paths.find { |path| File.exist?(path) }
|
|
82
|
+
|
|
83
|
+
if script_path.nil?
|
|
84
|
+
raise Error, "Script file not found. Searched in:\n#{search_paths.join("\n")}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@server_started = true
|
|
88
|
+
App.start(script_path, port: port, host: host)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The main module for streamlit-like API
|
|
92
|
+
module St
|
|
93
|
+
@@color = Pastel.new
|
|
94
|
+
# Get a value from the cache, recomputing only if dependencies changed
|
|
95
|
+
def self.compute(key, &block)
|
|
96
|
+
session = Showtime.session
|
|
97
|
+
registry = session.component_registry
|
|
98
|
+
|
|
99
|
+
# If a cached value exists and the component (key) is NOT in the dirty set,
|
|
100
|
+
# we can use the cached value. ComponentRegistry#mark_dirty handles propagation.
|
|
101
|
+
if session.has_value?(key) && !registry.dirty_components.include?(key)
|
|
102
|
+
Showtime::Logger.debug("Cache hit for key: #{@@color.bright_yellow(key)} (is present and not marked dirty).")
|
|
103
|
+
return session.get_value(key)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Log reason for recomputation if we reach here.
|
|
107
|
+
if !session.has_value?(key)
|
|
108
|
+
Showtime::Logger.debug("Cache miss for key: #{@@color.bright_yellow(key)}. Recomputing.")
|
|
109
|
+
else # Implies session.has_value?(key) is true AND registry.dirty_components.include?(key) is true
|
|
110
|
+
Showtime::Logger.debug("Key #{@@color.bright_yellow(key)} is marked dirty. Recomputing.")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
tracker = ComputeTracker.new do
|
|
114
|
+
result = block.call
|
|
115
|
+
session.update_value(key, result) # Store/update the computed value
|
|
116
|
+
result
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
registry.register_dependencies(key, tracker.accessed_keys)
|
|
120
|
+
|
|
121
|
+
tracker.result
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.get(key, default = nil, &block)
|
|
125
|
+
session = Showtime.session
|
|
126
|
+
|
|
127
|
+
# Track this access for dependency tracking
|
|
128
|
+
Thread.current[:compute_tracker]&.accessed_keys&.add(key)
|
|
129
|
+
|
|
130
|
+
# Return cached value if available
|
|
131
|
+
if session.has_value?(key)
|
|
132
|
+
Showtime::Logger.debug("Cache hit for key: #{@@color.bright_yellow(key)}")
|
|
133
|
+
return session.get_value(key, default)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# If block given, compute and cache
|
|
137
|
+
return compute(key, &block) if block_given?
|
|
138
|
+
|
|
139
|
+
default
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.set(key, value)
|
|
143
|
+
Showtime.session.update_value(key, value)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.clear_session
|
|
147
|
+
Showtime.session.clear_values
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.path(relative_path)
|
|
151
|
+
Showtime::Helpers.absolute_path(relative_path)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Make the St module accessible globally
|
|
157
|
+
St = Showtime::St
|