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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +179 -0
  4. data/bin/showtime +47 -0
  5. data/lib/showtime/app.rb +399 -0
  6. data/lib/showtime/charts.rb +229 -0
  7. data/lib/showtime/component_registry.rb +38 -0
  8. data/lib/showtime/components/Components.md +309 -0
  9. data/lib/showtime/components/alerts.rb +83 -0
  10. data/lib/showtime/components/base.rb +63 -0
  11. data/lib/showtime/components/charts.rb +119 -0
  12. data/lib/showtime/components/data.rb +328 -0
  13. data/lib/showtime/components/inputs.rb +390 -0
  14. data/lib/showtime/components/layout.rb +135 -0
  15. data/lib/showtime/components/media.rb +73 -0
  16. data/lib/showtime/components/sidebar.rb +130 -0
  17. data/lib/showtime/components/text.rb +156 -0
  18. data/lib/showtime/components.rb +18 -0
  19. data/lib/showtime/compute_tracker.rb +21 -0
  20. data/lib/showtime/helpers.rb +53 -0
  21. data/lib/showtime/logger.rb +143 -0
  22. data/lib/showtime/public/.vite/manifest.json +34 -0
  23. data/lib/showtime/public/assets/antd-3aDVoXqG.js +447 -0
  24. data/lib/showtime/public/assets/charts-iowb_sWQ.js +3858 -0
  25. data/lib/showtime/public/assets/index-B2b3lWS5.js +43 -0
  26. data/lib/showtime/public/assets/index-M6NVamDM.css +1 -0
  27. data/lib/showtime/public/assets/react-BE6xecJX.js +32 -0
  28. data/lib/showtime/public/index.html +19 -0
  29. data/lib/showtime/public/letter.png +0 -0
  30. data/lib/showtime/public/logo.png +0 -0
  31. data/lib/showtime/release.rb +108 -0
  32. data/lib/showtime/session.rb +131 -0
  33. data/lib/showtime/version.rb +3 -0
  34. data/lib/showtime/views/index.erb +32 -0
  35. data/lib/showtime.rb +157 -0
  36. 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,3 @@
1
+ module Showtime
2
+ VERSION = "0.1.1"
3
+ 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