appydave-tools 0.39.0 → 0.40.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5c03f22b90ef02e4609a1d50bf1c1d06cf03b782a24cd9196463a746d91f33d
4
- data.tar.gz: 61bc3604ae50ad6d9cc037c1df8be8c9d677a11aab23387c5fe74974e0bb3da4
3
+ metadata.gz: 9610c23fa022740f9082e2bd0086bf7727cfe96437309b099a1bc5fbe55f58ea
4
+ data.tar.gz: d556ee4ced131cfc9f7b8244edf9c2c464d5d3692a669cb0f7102831fccf1689
5
5
  SHA512:
6
- metadata.gz: 8c6ebbbce5cea68d4168d606521a57162e2dcee80d91dfd11b3785ca2e8fa722d1199553a64b51c9734c89869e725626e0cbcef6089e73e694c9b16b12079abb
7
- data.tar.gz: b95e4edcd87705d08b2ebdf0c5b93884cf59386da8d169e21aea881119b5f8eb14d555c8f5c6b8c83ba236904b8f99d102fc3a192f9db4d06fc814d7405731c0
6
+ metadata.gz: f337f930defe037888a8ebc8dd19f6562519b39170b33a8944e68cc050059a65902d41c788d6cafa2c6132b2d2ee6f408ad54a420ce68c1bb74fb392ab9dc716
7
+ data.tar.gz: f7f0933afa44c8e3a79aa3ceb6b53db950d0acb9e3b5ba17daf3acc46eafc97afd19071c0e335485d3345c08c9218c2285f5dcc0f74332bd87a3bf136a7dc210
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.39.0](https://github.com/appydave/appydave-tools/compare/v0.38.0...v0.39.0) (2025-11-21)
2
+
3
+
4
+ ### Features
5
+
6
+ * create BrandResolver class to centralize brand name resolution and eliminate ~30 lines of duplication from Config.expand_brand, fixing case-sensitivity bugs ([d6cce4b](https://github.com/appydave/appydave-tools/commit/d6cce4b0c1df6d7efcd40db52e80534c4014fdb5))
7
+
1
8
  # [0.38.0](https://github.com/appydave/appydave-tools/compare/v0.37.0...v0.38.0) (2025-11-21)
2
9
 
3
10
 
@@ -3,7 +3,33 @@
3
3
  module Appydave
4
4
  module Tools
5
5
  module Configuration
6
- # Configuration class for handling multiple configurations
6
+ # Central configuration management for appydave-tools
7
+ #
8
+ # Thread-safe singleton pattern with memoization for registered configurations.
9
+ # Calling `Config.configure` multiple times is safe and idempotent.
10
+ #
11
+ # @example Basic usage
12
+ # Config.configure # Load default configuration (idempotent)
13
+ # Config.settings.video_projects_root # Access settings
14
+ # Config.brands.get_brand('appydave') # Access brands
15
+ #
16
+ # @example DAM module usage pattern
17
+ # # Config.configure called once at module load time
18
+ # # All subsequent calls within DAM classes are no-ops (memoized)
19
+ # def some_method
20
+ # Config.configure # Safe to call - returns immediately if already configured
21
+ # brand = Config.brands.get_brand('appydave')
22
+ # end
23
+ #
24
+ # @example Registered configurations
25
+ # Config.settings # => SettingsConfig instance
26
+ # Config.brands # => BrandsConfig instance
27
+ # Config.channels # => ChannelsConfig instance
28
+ # Config.youtube_automation # => YoutubeAutomationConfig instance
29
+ #
30
+ # @note Configuration instances are created once on first registration and reused
31
+ # for all subsequent accesses. This prevents unnecessary file I/O and ensures
32
+ # consistent state across the application.
7
33
  class Config
8
34
  class << self
9
35
  include KLog::Logging
@@ -12,6 +38,25 @@ module Appydave
12
38
  attr_reader :configurations
13
39
  attr_reader :default_block
14
40
 
41
+ # Load configuration using either provided block or default configuration
42
+ #
43
+ # This method is idempotent and thread-safe. Calling it multiple times
44
+ # has no negative side effects - configurations are memoized on first call.
45
+ #
46
+ # @yield [Config] configuration object for manual setup
47
+ # @return [void]
48
+ # @raise [Error] if no block provided and no default_block set
49
+ #
50
+ # @example With block (manual configuration)
51
+ # Config.configure do |config|
52
+ # config.config_path = '/custom/path'
53
+ # config.register(:settings, SettingsConfig)
54
+ # end
55
+ #
56
+ # @example Without block (uses default_block)
57
+ # Config.set_default { |config| config.register(:settings, SettingsConfig) }
58
+ # Config.configure # Uses default_block
59
+ # Config.configure # Safe to call again - no-op due to memoization
15
60
  def configure
16
61
  if block_given?
17
62
  yield self
@@ -23,6 +68,23 @@ module Appydave
23
68
  ensure_config_directory
24
69
  end
25
70
 
71
+ # Register a configuration class with memoization
72
+ #
73
+ # Creates a single instance of the configuration class on first call.
74
+ # Subsequent calls return the same instance (memoized). This prevents
75
+ # unnecessary file I/O and ensures consistent configuration state.
76
+ #
77
+ # @param key [Symbol] configuration identifier (e.g., :settings, :brands)
78
+ # @param klass [Class] configuration class to instantiate
79
+ # @return [Object] configuration instance
80
+ #
81
+ # @example
82
+ # Config.register(:settings, SettingsConfig)
83
+ # Config.settings # => SettingsConfig instance (created on first access)
84
+ # Config.settings # => Same instance (memoized)
85
+ #
86
+ # @note This method implements lazy initialization - the configuration
87
+ # instance is only created when first accessed, not at registration time.
26
88
  def register(key, klass)
27
89
  @configurations ||= {}
28
90
  # Only create new instance if not already registered (prevents multiple reloads)
@@ -30,10 +92,26 @@ module Appydave
30
92
  end
31
93
 
32
94
  # Reset all configurations (primarily for testing)
95
+ #
96
+ # Clears all memoized configuration instances. Use this in test teardown
97
+ # to ensure each test starts with a clean configuration state.
98
+ #
99
+ # @return [void]
100
+ #
101
+ # @example RSpec usage
102
+ # after { Config.reset }
33
103
  def reset
34
104
  @configurations = nil
35
105
  end
36
106
 
107
+ # Dynamic accessor for registered configurations
108
+ #
109
+ # Provides method-style access to registered configuration instances.
110
+ # Called when accessing Config.settings, Config.brands, etc.
111
+ #
112
+ # @param method_name [Symbol] configuration key
113
+ # @return [Object] configuration instance
114
+ # @raise [Error] if configurations not registered or key not found
37
115
  def method_missing(method_name, *_args)
38
116
  raise Appydave::Tools::Error, 'Configuration has never been registered' if @configurations.nil?
39
117
  raise Appydave::Tools::Error, "Configuration not available: #{method_name}" unless @configurations.key?(method_name)
@@ -45,18 +123,27 @@ module Appydave
45
123
  @configurations.key?(method_name) || super
46
124
  end
47
125
 
126
+ # Save all registered configurations to their respective files
127
+ # @return [void]
48
128
  def save
49
129
  configurations.each_value(&:save)
50
130
  end
51
131
 
132
+ # Set default configuration block used when configure called without block
133
+ # @yield [Config] configuration block to execute by default
134
+ # @return [Proc] the stored block
52
135
  def set_default(&block)
53
136
  @default_block = block
54
137
  end
55
138
 
139
+ # Load all registered configurations from their respective files
140
+ # @return [void]
56
141
  def load
57
142
  configurations.each_value(&:load)
58
143
  end
59
144
 
145
+ # Open configuration directory in VS Code
146
+ # @return [void]
60
147
  def edit
61
148
  ensure_config_directory
62
149
  puts "Edit configuration: #{config_path}"
@@ -64,6 +151,8 @@ module Appydave
64
151
  Open3.capture3(open_vscode)
65
152
  end
66
153
 
154
+ # Debug output for all configurations
155
+ # @return [void]
67
156
  def debug
68
157
  log.kv 'Configuration Path', config_path
69
158
  configurations.each_value(&:debug)
@@ -74,6 +163,9 @@ module Appydave
74
163
  # configurations.each_value(&:print)
75
164
  # end
76
165
 
166
+ # Print specific configurations or all if no keys provided
167
+ # @param keys [Array<String, Symbol>] configuration keys to print
168
+ # @return [void]
77
169
  def print(*keys)
78
170
  if keys.empty?
79
171
  keys = configurations.keys
@@ -140,6 +140,7 @@ module Appydave
140
140
  skipped = 0
141
141
  failed = 0
142
142
 
143
+ # rubocop:disable Metrics/BlockLength
143
144
  files.each do |file|
144
145
  relative_path = file.sub("#{staging_dir}/", '')
145
146
 
@@ -158,12 +159,31 @@ module Appydave
158
159
  if local_md5 == s3_md5
159
160
  puts " ⏭️ Skipped: #{relative_path} (unchanged)"
160
161
  skipped += 1
161
- elsif upload_file(file, s3_path, dry_run: dry_run)
162
- uploaded += 1
163
162
  else
164
- failed += 1
163
+ # Warn if we're about to overwrite an existing S3 file
164
+ if s3_md5 && s3_md5 != local_md5
165
+ puts " ⚠️ Warning: #{relative_path} exists in S3 with different content"
166
+
167
+ # Try to get S3 timestamp for comparison
168
+ s3_file_info = get_s3_file_info(s3_path)
169
+ if s3_file_info && s3_file_info['LastModified']
170
+ s3_time = s3_file_info['LastModified']
171
+ local_time = File.mtime(file)
172
+ puts " S3: #{s3_time.strftime('%Y-%m-%d %H:%M')} | Local: #{local_time.strftime('%Y-%m-%d %H:%M')}"
173
+
174
+ puts ' ⚠️ S3 file is NEWER than local - you may be overwriting recent changes!' if s3_time > local_time
175
+ end
176
+ puts ' Uploading will overwrite S3 version...'
177
+ end
178
+
179
+ if upload_file(file, s3_path, dry_run: dry_run)
180
+ uploaded += 1
181
+ else
182
+ failed += 1
183
+ end
165
184
  end
166
185
  end
186
+ # rubocop:enable Metrics/BlockLength
167
187
 
168
188
  puts ''
169
189
  puts '✅ Upload complete!'
@@ -207,13 +227,29 @@ module Appydave
207
227
  if local_md5 == s3_md5
208
228
  puts " ⏭️ Skipped: #{relative_path} (unchanged)"
209
229
  skipped += 1
210
- elsif download_file(key, local_file, dry_run: dry_run)
211
- downloaded += 1
212
230
  else
213
- failed += 1
231
+ # Warn if we're about to overwrite an existing local file
232
+ if local_md5 && local_md5 != s3_md5
233
+ puts " ⚠️ Warning: #{relative_path} exists locally with different content"
234
+
235
+ # Compare timestamps
236
+ if s3_file['LastModified'] && File.exist?(local_file)
237
+ s3_time = s3_file['LastModified']
238
+ local_time = File.mtime(local_file)
239
+ puts " S3: #{s3_time.strftime('%Y-%m-%d %H:%M')} | Local: #{local_time.strftime('%Y-%m-%d %H:%M')}"
240
+
241
+ puts ' ⚠️ Local file is NEWER than S3 - you may be overwriting recent changes!' if local_time > s3_time
242
+ end
243
+ puts ' Downloading will overwrite local version...'
244
+ end
245
+
246
+ if download_file(key, local_file, dry_run: dry_run)
247
+ downloaded += 1
248
+ else
249
+ failed += 1
250
+ end
214
251
  end
215
252
  end
216
-
217
253
  puts ''
218
254
  puts '✅ Download complete!'
219
255
  puts " Downloaded: #{downloaded}, Skipped: #{skipped}, Failed: #{failed}"
@@ -634,13 +670,31 @@ module Appydave
634
670
  {
635
671
  'Key' => obj.key,
636
672
  'Size' => obj.size,
637
- 'ETag' => obj.etag
673
+ 'ETag' => obj.etag,
674
+ 'LastModified' => obj.last_modified
638
675
  }
639
676
  end
640
677
  rescue Aws::S3::Errors::ServiceError
641
678
  []
642
679
  end
643
680
 
681
+ # Get full S3 file info including timestamp
682
+ def get_s3_file_info(s3_key)
683
+ response = s3_client.head_object(
684
+ bucket: brand_info.aws.s3_bucket,
685
+ key: s3_key
686
+ )
687
+
688
+ {
689
+ 'Key' => s3_key,
690
+ 'Size' => response.content_length,
691
+ 'ETag' => response.etag,
692
+ 'LastModified' => response.last_modified
693
+ }
694
+ rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::ServiceError
695
+ nil
696
+ end
697
+
644
698
  # List local files in staging directory
645
699
  def list_local_files(staging_dir)
646
700
  return {} unless Dir.exist?(staging_dir)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Appydave
4
4
  module Tools
5
- VERSION = '0.39.0'
5
+ VERSION = '0.40.0'
6
6
  end
7
7
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appydave-tools",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "AppyDave YouTube Automation Tools",
5
5
  "scripts": {
6
6
  "release": "semantic-release"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appydave-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.39.0
4
+ version: 0.40.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cruwys