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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/appydave/tools/configuration/config.rb +93 -1
- data/lib/appydave/tools/dam/s3_operations.rb +62 -8
- data/lib/appydave/tools/version.rb +1 -1
- data/package.json +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9610c23fa022740f9082e2bd0086bf7727cfe96437309b099a1bc5fbe55f58ea
|
|
4
|
+
data.tar.gz: d556ee4ced131cfc9f7b8244edf9c2c464d5d3692a669cb0f7102831fccf1689
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
data/package.json
CHANGED