rospatent 1.0.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 +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +1247 -0
- data/lib/generators/rospatent/install/install_generator.rb +21 -0
- data/lib/generators/rospatent/install/templates/README +29 -0
- data/lib/generators/rospatent/install/templates/initializer.rb +24 -0
- data/lib/rospatent/cache.rb +282 -0
- data/lib/rospatent/client.rb +698 -0
- data/lib/rospatent/configuration.rb +136 -0
- data/lib/rospatent/errors.rb +127 -0
- data/lib/rospatent/input_validator.rb +306 -0
- data/lib/rospatent/logger.rb +286 -0
- data/lib/rospatent/patent_parser.rb +141 -0
- data/lib/rospatent/railtie.rb +26 -0
- data/lib/rospatent/search.rb +222 -0
- data/lib/rospatent/version.rb +5 -0
- data/lib/rospatent.rb +117 -0
- metadata +167 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
|
5
|
+
module Rospatent
|
6
|
+
module Generators
|
7
|
+
# Generator for installing Rospatent configuration into a Rails application
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
desc "Creates a Rospatent initializer for a Rails application"
|
11
|
+
|
12
|
+
def create_initializer_file
|
13
|
+
template "initializer.rb", "config/initializers/rospatent.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
def show_readme
|
17
|
+
readme "README" if behavior == :invoke
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Rospatent Ruby - Rails Integration
|
2
|
+
|
3
|
+
The Rospatent Ruby gem has been configured for your Rails application.
|
4
|
+
|
5
|
+
## Configuration
|
6
|
+
|
7
|
+
You can find and modify the configuration in:
|
8
|
+
`config/initializers/rospatent.rb`
|
9
|
+
|
10
|
+
## Environment Variables
|
11
|
+
|
12
|
+
For security, it's recommended to use environment variables for sensitive information:
|
13
|
+
|
14
|
+
```
|
15
|
+
ROSPATENT_API_TOKEN=your_jwt_token
|
16
|
+
```
|
17
|
+
|
18
|
+
You can add these to your `.env` file if you're using the dotenv gem, or set them up in your deployment environment.
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
You can now use the Rospatent API client in your Rails application:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
# In a controller or service
|
26
|
+
results = Rospatent.client.search(q: "search_term")
|
27
|
+
```
|
28
|
+
|
29
|
+
For more usage details, see the gem's README.md file.
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Rospatent API client configuration
|
4
|
+
# Documentation: https://online.rospatent.gov.ru/open-data/open-api
|
5
|
+
Rospatent.configure do |config|
|
6
|
+
# API URL (default: https://searchplatform.rospatent.gov.ru)
|
7
|
+
# config.api_url = ENV.fetch("ROSPATENT_API_URL", "https://searchplatform.rospatent.gov.ru")
|
8
|
+
|
9
|
+
# JWT Bearer token for API authorization - REQUIRED
|
10
|
+
# Obtain this from the Rospatent API administration
|
11
|
+
config.token = Rails.application.credentials.rospatent_api_token || ENV.fetch(
|
12
|
+
"ROSPATENT_API_TOKEN", nil
|
13
|
+
)
|
14
|
+
|
15
|
+
# Rails-specific environment integration
|
16
|
+
config.environment = Rails.env
|
17
|
+
config.cache_enabled = Rails.env.production?
|
18
|
+
config.log_level = Rails.env.production? ? :warn : :debug
|
19
|
+
|
20
|
+
# Optional: Override defaults if needed
|
21
|
+
# config.timeout = 30
|
22
|
+
# config.retry_count = 3
|
23
|
+
# config.user_agent = "YourApp/1.0"
|
24
|
+
end
|
@@ -0,0 +1,282 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "monitor"
|
4
|
+
|
5
|
+
module Rospatent
|
6
|
+
# Simple in-memory cache with TTL and size limits
|
7
|
+
class Cache
|
8
|
+
include MonitorMixin
|
9
|
+
|
10
|
+
# Cache entry with value and expiration time
|
11
|
+
CacheEntry = Struct.new(:value, :expires_at, :created_at, :access_count) do
|
12
|
+
def expired?
|
13
|
+
Time.now > expires_at
|
14
|
+
end
|
15
|
+
|
16
|
+
def touch!
|
17
|
+
self.access_count += 1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :stats
|
22
|
+
|
23
|
+
# Initialize a new cache
|
24
|
+
# @param ttl [Integer] Time to live in seconds
|
25
|
+
# @param max_size [Integer] Maximum number of entries
|
26
|
+
def initialize(ttl: 300, max_size: 1000)
|
27
|
+
super()
|
28
|
+
@ttl = ttl
|
29
|
+
@max_size = max_size
|
30
|
+
@store = {}
|
31
|
+
@access_order = []
|
32
|
+
@stats = {
|
33
|
+
hits: 0,
|
34
|
+
misses: 0,
|
35
|
+
evictions: 0,
|
36
|
+
expired: 0
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get a value from the cache
|
41
|
+
# @param key [String] Cache key
|
42
|
+
# @return [Object, nil] Cached value or nil if not found/expired
|
43
|
+
def get(key)
|
44
|
+
synchronize do
|
45
|
+
entry = @store[key]
|
46
|
+
|
47
|
+
unless entry
|
48
|
+
@stats[:misses] += 1
|
49
|
+
return nil
|
50
|
+
end
|
51
|
+
|
52
|
+
if entry.expired?
|
53
|
+
delete_entry(key)
|
54
|
+
@stats[:expired] += 1
|
55
|
+
@stats[:misses] += 1
|
56
|
+
return nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# Update access order for LRU
|
60
|
+
@access_order.delete(key)
|
61
|
+
@access_order.push(key)
|
62
|
+
|
63
|
+
entry.touch!
|
64
|
+
@stats[:hits] += 1
|
65
|
+
entry.value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Set a value in the cache
|
70
|
+
# @param key [String] Cache key
|
71
|
+
# @param value [Object] Value to cache
|
72
|
+
# @param ttl [Integer, nil] Custom TTL for this entry (optional)
|
73
|
+
def set(key, value, ttl: nil)
|
74
|
+
synchronize do
|
75
|
+
effective_ttl = ttl || @ttl
|
76
|
+
expires_at = Time.now + effective_ttl
|
77
|
+
|
78
|
+
entry = CacheEntry.new(value, expires_at, Time.now, 0)
|
79
|
+
|
80
|
+
# Remove existing entry if present
|
81
|
+
@access_order.delete(key) if @store.key?(key)
|
82
|
+
|
83
|
+
@store[key] = entry
|
84
|
+
@access_order.push(key)
|
85
|
+
|
86
|
+
# Evict entries if over size limit
|
87
|
+
evict_if_needed
|
88
|
+
|
89
|
+
value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Check if a key exists and is not expired
|
94
|
+
# @param key [String] Cache key
|
95
|
+
# @return [Boolean] true if key exists and is valid
|
96
|
+
def key?(key)
|
97
|
+
synchronize do
|
98
|
+
entry = @store[key]
|
99
|
+
return false unless entry
|
100
|
+
|
101
|
+
if entry.expired?
|
102
|
+
delete_entry(key)
|
103
|
+
@stats[:expired] += 1
|
104
|
+
return false
|
105
|
+
end
|
106
|
+
|
107
|
+
true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Delete a specific key
|
112
|
+
# @param key [String] Cache key
|
113
|
+
# @return [Object, nil] Deleted value or nil if not found
|
114
|
+
def delete(key)
|
115
|
+
synchronize do
|
116
|
+
entry = @store.delete(key)
|
117
|
+
@access_order.delete(key)
|
118
|
+
entry&.value
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Clear all entries from the cache
|
123
|
+
def clear
|
124
|
+
synchronize do
|
125
|
+
@store.clear
|
126
|
+
@access_order.clear
|
127
|
+
reset_stats
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Get current cache size
|
132
|
+
# @return [Integer] Number of entries in cache
|
133
|
+
def size
|
134
|
+
synchronize { @store.size }
|
135
|
+
end
|
136
|
+
|
137
|
+
# Check if cache is empty
|
138
|
+
# @return [Boolean] true if cache has no entries
|
139
|
+
def empty?
|
140
|
+
synchronize { @store.empty? }
|
141
|
+
end
|
142
|
+
|
143
|
+
# Get cache statistics
|
144
|
+
# @return [Hash] Statistics including hits, misses, hit rate, etc.
|
145
|
+
def statistics
|
146
|
+
synchronize do
|
147
|
+
total_requests = @stats[:hits] + @stats[:misses]
|
148
|
+
hit_rate = total_requests.positive? ? (@stats[:hits].to_f / total_requests * 100).round(2) : 0
|
149
|
+
|
150
|
+
@stats.merge(
|
151
|
+
size: @store.size,
|
152
|
+
total_requests: total_requests,
|
153
|
+
hit_rate_percent: hit_rate
|
154
|
+
)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Clean up expired entries
|
159
|
+
# @return [Integer] Number of expired entries removed
|
160
|
+
def cleanup_expired
|
161
|
+
synchronize do
|
162
|
+
expired_keys = []
|
163
|
+
@store.each do |key, entry|
|
164
|
+
expired_keys << key if entry.expired?
|
165
|
+
end
|
166
|
+
|
167
|
+
expired_keys.each { |key| delete_entry(key) }
|
168
|
+
@stats[:expired] += expired_keys.size
|
169
|
+
|
170
|
+
expired_keys.size
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Fetch value with fallback block or default value
|
175
|
+
# @param key [String] Cache key
|
176
|
+
# @param default_value [Object, nil] Default value to return if cache miss (optional)
|
177
|
+
# @param ttl [Integer, nil] Custom TTL for this entry
|
178
|
+
# @yield Block to execute if cache miss
|
179
|
+
# @return [Object] Cached value, default value, or result of block
|
180
|
+
def fetch(key, default_value = nil, ttl: nil)
|
181
|
+
value = get(key)
|
182
|
+
return value unless value.nil?
|
183
|
+
|
184
|
+
result = if default_value
|
185
|
+
default_value
|
186
|
+
elsif block_given?
|
187
|
+
yield
|
188
|
+
else
|
189
|
+
return nil
|
190
|
+
end
|
191
|
+
|
192
|
+
set(key, result, ttl: ttl) unless result.nil?
|
193
|
+
result
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
# Delete an entry and update access order
|
199
|
+
# @param key [String] Key to delete
|
200
|
+
def delete_entry(key)
|
201
|
+
@store.delete(key)
|
202
|
+
@access_order.delete(key)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Evict least recently used entries if over size limit
|
206
|
+
def evict_if_needed
|
207
|
+
while @store.size > @max_size
|
208
|
+
lru_key = @access_order.shift
|
209
|
+
break unless lru_key
|
210
|
+
|
211
|
+
@store.delete(lru_key)
|
212
|
+
@stats[:evictions] += 1
|
213
|
+
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Reset statistics counters
|
218
|
+
def reset_stats
|
219
|
+
@stats = {
|
220
|
+
hits: 0,
|
221
|
+
misses: 0,
|
222
|
+
evictions: 0,
|
223
|
+
expired: 0
|
224
|
+
}
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Null cache implementation for when caching is disabled
|
229
|
+
class NullCache
|
230
|
+
def get(_key)
|
231
|
+
nil
|
232
|
+
end
|
233
|
+
|
234
|
+
def set(_key, value, ttl: nil)
|
235
|
+
value
|
236
|
+
end
|
237
|
+
|
238
|
+
def key?(_key)
|
239
|
+
false
|
240
|
+
end
|
241
|
+
|
242
|
+
def delete(_key)
|
243
|
+
nil
|
244
|
+
end
|
245
|
+
|
246
|
+
def clear
|
247
|
+
# no-op
|
248
|
+
end
|
249
|
+
|
250
|
+
def size
|
251
|
+
0
|
252
|
+
end
|
253
|
+
|
254
|
+
def empty?
|
255
|
+
true
|
256
|
+
end
|
257
|
+
|
258
|
+
def statistics
|
259
|
+
{
|
260
|
+
hits: 0,
|
261
|
+
misses: 0,
|
262
|
+
evictions: 0,
|
263
|
+
expired: 0,
|
264
|
+
size: 0,
|
265
|
+
total_requests: 0,
|
266
|
+
hit_rate_percent: 0
|
267
|
+
}
|
268
|
+
end
|
269
|
+
|
270
|
+
def cleanup_expired
|
271
|
+
0
|
272
|
+
end
|
273
|
+
|
274
|
+
def fetch(_key, default_value = nil, ttl: nil)
|
275
|
+
if default_value
|
276
|
+
default_value
|
277
|
+
elsif block_given?
|
278
|
+
yield
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|