nexus_mods 0.3.0 → 0.4.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 +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
|