nexus_mods 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/nexus_mods/api_client.rb +22 -15
- data/lib/nexus_mods/cacheable_api.rb +8 -2
- data/lib/nexus_mods/cacheable_with_expiry.rb +14 -12
- data/lib/nexus_mods/core_extensions/cacheable/cache_adapters/persistent_json_adapter.rb +38 -2
- data/lib/nexus_mods/version.rb +1 -1
- data/lib/nexus_mods.rb +3 -0
- data/spec/nexus_mods_test/helpers.rb +16 -0
- data/spec/nexus_mods_test/scenarios/nexus_mods_caching_spec.rb +69 -0
- 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: 79e4d81970dd1549db43105a2a155b6b275692df4f06827e2234ab02fce6276b
|
4
|
+
data.tar.gz: 4d96df8a39784c8a2eb6fb9bd72a247f3cafa2765aa03059dfc661afd9db7a38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c13d188694e09dc06998846791322a4887bbb5163ff634d5e6f813abca187a6a2dc8aeb102e040b43812d6276feb6c1ee59665c5e5e408c6acbff4dca0539b3
|
7
|
+
data.tar.gz: 3e37eb153f00bb6335efb8e0b377065f4b0613cbef46066a1a5d7095b1e1767b1786d59f28681298e75be49e1cfeb739ea6ce2f7b13fefc6b28362b27d7536b5
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
# [v0.4.0](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.3.0...v0.4.0) (2023-04-10 10:15:41)
|
2
|
+
|
3
|
+
### Features
|
4
|
+
|
5
|
+
* [[Feature] Add persistent API caching in files](https://github.com/Muriel-Salvan/nexus_mods/commit/f124ecf58b0f478ecdca5f6ad2309779d1ab67ac)
|
6
|
+
|
1
7
|
# [v0.3.0](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.2.0...v0.3.0) (2023-04-10 09:13:43)
|
2
8
|
|
3
9
|
### Features
|
@@ -12,6 +12,13 @@ class NexusMods
|
|
12
12
|
|
13
13
|
include CacheableApi
|
14
14
|
|
15
|
+
# Default expiry times, in seconds
|
16
|
+
DEFAULT_API_CACHE_EXPIRY = {
|
17
|
+
games: 24 * 60 * 60,
|
18
|
+
mod: 24 * 60 * 60,
|
19
|
+
mod_files: 24 * 60 * 60
|
20
|
+
}
|
21
|
+
|
15
22
|
# Constructor
|
16
23
|
#
|
17
24
|
# Parameters::
|
@@ -31,8 +38,9 @@ class NexusMods
|
|
31
38
|
logger: Logger.new($stdout)
|
32
39
|
)
|
33
40
|
@api_key = api_key
|
34
|
-
|
41
|
+
@api_cache_expiry = DEFAULT_API_CACHE_EXPIRY.merge(api_cache_expiry)
|
35
42
|
@api_cache_file = api_cache_file
|
43
|
+
ApiClient.api_client = self
|
36
44
|
@logger = logger
|
37
45
|
# Initialize our HTTP client
|
38
46
|
@http_cache = http_cache_file.nil? ? nil : FileCache.new(http_cache_file)
|
@@ -96,15 +104,15 @@ class NexusMods
|
|
96
104
|
case key_components[0]
|
97
105
|
when 'games'
|
98
106
|
if key_components[1].nil?
|
99
|
-
ApiClient.api_cache_expiry[:games]
|
107
|
+
ApiClient.api_client.api_cache_expiry[:games]
|
100
108
|
else
|
101
109
|
case key_components[2]
|
102
110
|
when 'mods'
|
103
111
|
case key_components[4]
|
104
112
|
when nil
|
105
|
-
ApiClient.api_cache_expiry[:mod]
|
113
|
+
ApiClient.api_client.api_cache_expiry[:mod]
|
106
114
|
when 'files'
|
107
|
-
ApiClient.api_cache_expiry[:mod_files]
|
115
|
+
ApiClient.api_client.api_cache_expiry[:mod_files]
|
108
116
|
else
|
109
117
|
raise "Unknown API path: #{key}"
|
110
118
|
end
|
@@ -118,6 +126,9 @@ class NexusMods
|
|
118
126
|
else
|
119
127
|
raise "Unknown API path: #{key}"
|
120
128
|
end
|
129
|
+
end,
|
130
|
+
on_cache_update: proc do
|
131
|
+
ApiClient.api_client.save_api_cache
|
121
132
|
end
|
122
133
|
)
|
123
134
|
|
@@ -146,26 +157,22 @@ class NexusMods
|
|
146
157
|
return unless @api_cache_file
|
147
158
|
|
148
159
|
FileUtils.mkdir_p(File.dirname(@api_cache_file))
|
149
|
-
Cacheable.cache_adapter.
|
160
|
+
Cacheable.cache_adapter.save(@api_cache_file)
|
150
161
|
end
|
151
162
|
|
163
|
+
# Some attributes exposed for the cacheable feature to work
|
164
|
+
attr_reader :api_cache_expiry
|
165
|
+
|
152
166
|
private
|
153
167
|
|
154
168
|
class << self
|
155
169
|
|
156
|
-
#
|
157
|
-
attr_accessor :
|
170
|
+
# ApiClient: The API client to be used by the cacheable adapter (singleton pattern)
|
171
|
+
attr_accessor :api_client
|
158
172
|
|
159
173
|
end
|
160
174
|
|
161
|
-
|
162
|
-
DEFAULT_API_CACHE_EXPIRY = {
|
163
|
-
games: 24 * 60 * 60,
|
164
|
-
mod: 24 * 60 * 60,
|
165
|
-
mod_files: 24 * 60 * 60
|
166
|
-
}
|
167
|
-
|
168
|
-
@api_cache_expiry = DEFAULT_API_CACHE_EXPIRY
|
175
|
+
@api_client = nil
|
169
176
|
|
170
177
|
# Get the real URI to query for a given API path
|
171
178
|
#
|
@@ -28,7 +28,8 @@ class NexusMods
|
|
28
28
|
# * *key* (String): The key for which we want the expiry time in seconds
|
29
29
|
# * Result::
|
30
30
|
# * Integer: Corresponding expiry time
|
31
|
-
|
31
|
+
# * *on_cache_update* (Proc): Proc called when the cache has been updated
|
32
|
+
def cacheable_api(*original_method_names, expiry_from_key:, on_cache_update:)
|
32
33
|
cacheable_with_expiry(
|
33
34
|
*original_method_names,
|
34
35
|
key_format: lambda do |target, method_name, method_args, method_kwargs|
|
@@ -41,7 +42,12 @@ class NexusMods
|
|
41
42
|
method_kwargs.map { |key, value| "#{key}:#{value}" }
|
42
43
|
).join('/')
|
43
44
|
end,
|
44
|
-
expiry_from_key
|
45
|
+
expiry_from_key:,
|
46
|
+
cache_options: {
|
47
|
+
on_cache_update: proc do |_adapter, _key, _value, _options, _context|
|
48
|
+
on_cache_update.call
|
49
|
+
end
|
50
|
+
}
|
45
51
|
)
|
46
52
|
end
|
47
53
|
|
@@ -34,19 +34,21 @@ class NexusMods
|
|
34
34
|
*original_method_names,
|
35
35
|
**opts.merge(
|
36
36
|
{
|
37
|
-
cache_options: {
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
cache_options: (opts[:cache_options] || {}).merge(
|
38
|
+
{
|
39
|
+
expiry_from_key: opts[:expiry_from_key],
|
40
|
+
invalidate_if: proc do |key, options, context|
|
41
|
+
next true unless context['invalidate_time']
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
43
|
+
# Find if we know already the expiry for this key
|
44
|
+
expiry_cache[key] = options[:expiry_from_key].call(key) unless expiry_cache.key?(key)
|
45
|
+
expiry_cache[key].nil? || (Time.now.utc - Time.parse(context['invalidate_time']).utc > expiry_cache[key])
|
46
|
+
end,
|
47
|
+
update_context_after_fetch: proc do |_key, _value, _options, context|
|
48
|
+
context['invalidate_time'] = Time.now.utc.strftime('%FT%TUTC')
|
49
|
+
end
|
50
|
+
}
|
51
|
+
)
|
50
52
|
}
|
51
53
|
)
|
52
54
|
)
|
@@ -18,19 +18,26 @@ module Cacheable
|
|
18
18
|
# Parameters::
|
19
19
|
# * *key* (String): Key to be fetched
|
20
20
|
# * *options* (Hash): Cache options. The following options are interpreted by this fetch:
|
21
|
-
# * *invalidate_if* (Proc):
|
21
|
+
# * *invalidate_if* (Proc or nil): Optional code called to know if the cache should be invalidated for a given key:
|
22
22
|
# * Parameters::
|
23
23
|
# * *key* (String): The key for which we check the cache invalidation
|
24
24
|
# * *options* (Hash): Cache options linked to this key
|
25
25
|
# * *key_context* (Hash): Context linked to this key, that can be set using the update_context_after_fetch callback.
|
26
26
|
# * Result::
|
27
27
|
# * Boolean: Should we invalidate the cached value of this key?
|
28
|
-
# * *update_context_after_fetch* (Proc):
|
28
|
+
# * *update_context_after_fetch* (Proc or nil): Optional code called when the value has been fetched for real (without cache), used to update the context
|
29
29
|
# * Parameters::
|
30
30
|
# * *key* (String): The key for which we just fetched the value
|
31
31
|
# * *value* (Object): The value that has just been fetched
|
32
32
|
# * *options* (Hash): Cache options linked to this key
|
33
33
|
# * *key_context* (Hash): Context linked to this key, that is supposed to be updated in place by this callback
|
34
|
+
# * *on_cache_update* (Proc or nil): Optional code called once the cache has been updated
|
35
|
+
# * Parameters::
|
36
|
+
# * *adapter* (Object): Adapter that has the cache being updated
|
37
|
+
# * *key* (String): The key for which we just fetched the value
|
38
|
+
# * *value* (Object): The value that has just been fetched
|
39
|
+
# * *options* (Hash): Cache options linked to this key
|
40
|
+
# * *key_context* (Hash): Context linked to this key
|
34
41
|
# * CodeBlock: Code called to fetch the value if not in the cache
|
35
42
|
# Result::
|
36
43
|
# * Object: The value for this key
|
@@ -43,6 +50,8 @@ module Cacheable
|
|
43
50
|
value = yield
|
44
51
|
options[:update_context_after_fetch]&.call(key, value, options, key_context)
|
45
52
|
write(key, value)
|
53
|
+
options[:on_cache_update]&.call(self, key, value, options, key_context)
|
54
|
+
value
|
46
55
|
end
|
47
56
|
|
48
57
|
# Clear the cache
|
@@ -51,6 +60,33 @@ module Cacheable
|
|
51
60
|
super
|
52
61
|
end
|
53
62
|
|
63
|
+
# Save the cache and context into a JSON file
|
64
|
+
#
|
65
|
+
# Parameters::
|
66
|
+
# * *file* (String): The file to save to
|
67
|
+
def save(file)
|
68
|
+
# Remove from the context the keys that are not in the cache
|
69
|
+
File.write(
|
70
|
+
file,
|
71
|
+
JSON.dump(
|
72
|
+
{
|
73
|
+
'cache' => cache,
|
74
|
+
'context' => context.select { |key, _value| @cache.key?(key) }
|
75
|
+
}
|
76
|
+
)
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Load the cache and context from a JSON file
|
81
|
+
#
|
82
|
+
# Parameters::
|
83
|
+
# * *file* (String): The file to load from
|
84
|
+
def load(file)
|
85
|
+
loaded_content = JSON.parse(File.read(file))
|
86
|
+
@cache = loaded_content['cache']
|
87
|
+
@context = loaded_content['context']
|
88
|
+
end
|
89
|
+
|
54
90
|
private
|
55
91
|
|
56
92
|
attr_reader :context
|
data/lib/nexus_mods/version.rb
CHANGED
data/lib/nexus_mods.rb
CHANGED
@@ -44,6 +44,7 @@ class NexusMods
|
|
44
44
|
# * *games*: Expiry associated to queries on games [default: 1 day]
|
45
45
|
# * *mod*: Expiry associated to queries on mod [default: 1 day]
|
46
46
|
# * *mod_files*: Expiry associated to queries on mod files [default: 1 day]
|
47
|
+
# * *api_cache_file* (String): File used to store the NexusMods API cache, or nil for no cache [default: "#{Dir.tmpdir}/nexus_mods_api_cache.json"]
|
47
48
|
# * *logger* (Logger): The logger to be used for log messages [default: Logger.new(STDOUT)]
|
48
49
|
def initialize(
|
49
50
|
api_key: nil,
|
@@ -52,6 +53,7 @@ class NexusMods
|
|
52
53
|
file_id: 1,
|
53
54
|
http_cache_file: "#{Dir.tmpdir}/nexus_mods_http_cache.json",
|
54
55
|
api_cache_expiry: {},
|
56
|
+
api_cache_file: "#{Dir.tmpdir}/nexus_mods_api_cache.json",
|
55
57
|
logger: Logger.new($stdout)
|
56
58
|
)
|
57
59
|
@game_domain_name = game_domain_name
|
@@ -63,6 +65,7 @@ class NexusMods
|
|
63
65
|
api_key:,
|
64
66
|
http_cache_file:,
|
65
67
|
api_cache_expiry:,
|
68
|
+
api_cache_file:,
|
66
69
|
logger:
|
67
70
|
)
|
68
71
|
|
@@ -55,6 +55,9 @@ module NexusModsTest
|
|
55
55
|
def nexus_mods(**args)
|
56
56
|
if @nexus_mods.nil?
|
57
57
|
args[:api_key] = MOCKED_API_KEY unless args.key?(:api_key)
|
58
|
+
# By default running tests should not persistent cache files
|
59
|
+
args[:http_cache_file] = nil unless args.key?(:http_cache_file)
|
60
|
+
args[:api_cache_file] = nil unless args.key?(:api_cache_file)
|
58
61
|
# Redirect any log into a string so that they don't pollute the tests output and they could be asserted.
|
59
62
|
nexus_mods_logger = StringIO.new
|
60
63
|
args[:logger] = Logger.new(nexus_mods_logger)
|
@@ -134,6 +137,19 @@ module NexusModsTest
|
|
134
137
|
)
|
135
138
|
end
|
136
139
|
|
140
|
+
# Setup an API cache file to be used (does not create it)
|
141
|
+
#
|
142
|
+
# Parameters::
|
143
|
+
# * CodeBlock: The code called when the API cache file is reserved
|
144
|
+
# * Parameters::
|
145
|
+
# * *api_cache_file* (String): File name to be used for the API cache file
|
146
|
+
def with_api_cache_file
|
147
|
+
api_cache_file = "#{Dir.tmpdir}/nexus_mods_test/api_cache.json"
|
148
|
+
FileUtils.mkdir_p(File.dirname(api_cache_file))
|
149
|
+
FileUtils.rm_f(api_cache_file)
|
150
|
+
yield api_cache_file
|
151
|
+
end
|
152
|
+
|
137
153
|
end
|
138
154
|
|
139
155
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
1
3
|
describe NexusMods do
|
2
4
|
|
3
5
|
context 'when testing caching' do
|
@@ -83,6 +85,73 @@ describe NexusMods do
|
|
83
85
|
expect(nexus_mods_instance.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq(mod_files)
|
84
86
|
end
|
85
87
|
|
88
|
+
context 'with file persistence' do
|
89
|
+
|
90
|
+
it 'persists API cache in a file' do
|
91
|
+
with_api_cache_file do |api_cache_file|
|
92
|
+
expect_validate_user
|
93
|
+
expect_http_call_to(
|
94
|
+
path: '/v1/games.json',
|
95
|
+
json: [
|
96
|
+
json_game100,
|
97
|
+
json_game101
|
98
|
+
]
|
99
|
+
)
|
100
|
+
nexus_mods(api_cache_file:).games
|
101
|
+
expect(File.exist?(api_cache_file)).to be true
|
102
|
+
expect(File.size(api_cache_file)).to be > 0
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'uses API cache from a file' do
|
107
|
+
with_api_cache_file do |api_cache_file|
|
108
|
+
expect_validate_user(times: 2)
|
109
|
+
expect_http_call_to(
|
110
|
+
path: '/v1/games.json',
|
111
|
+
json: [
|
112
|
+
json_game100,
|
113
|
+
json_game101
|
114
|
+
]
|
115
|
+
)
|
116
|
+
# Generate the cache first
|
117
|
+
games = nexus_mods(api_cache_file:).games
|
118
|
+
# Force a new instance of NexusMods API to run
|
119
|
+
@nexus_mods = nil
|
120
|
+
expect(nexus_mods(api_cache_file:).games).to eq games
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'completes the API cache from a file' do
|
125
|
+
with_api_cache_file do |api_cache_file|
|
126
|
+
expect_validate_user(times: 3)
|
127
|
+
# Generate the cache first for games only
|
128
|
+
expect_http_call_to(
|
129
|
+
path: '/v1/games.json',
|
130
|
+
json: [
|
131
|
+
json_game100,
|
132
|
+
json_game101
|
133
|
+
]
|
134
|
+
)
|
135
|
+
games = nexus_mods(api_cache_file:).games
|
136
|
+
# Force a new instance of NexusMods API to run
|
137
|
+
@nexus_mods = nil
|
138
|
+
# Complete the cache with a mod
|
139
|
+
expect_http_call_to(
|
140
|
+
path: '/v1/games/skyrimspecialedition/mods/2014.json',
|
141
|
+
json: json_complete_mod
|
142
|
+
)
|
143
|
+
mod = nexus_mods(api_cache_file:).mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014)
|
144
|
+
# Force a new instance of NexusMods API to run
|
145
|
+
@nexus_mods = nil
|
146
|
+
# Check that both API calls were cached correctly
|
147
|
+
nexus_mods_instance = nexus_mods(api_cache_file:)
|
148
|
+
expect(nexus_mods_instance.games).to eq games
|
149
|
+
expect(nexus_mods_instance.mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq mod
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
86
155
|
end
|
87
156
|
|
88
157
|
end
|