epo-ops 0.1.5
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/.gitignore +37 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +34 -0
- data/Rakefile +10 -0
- data/epo-ops.gemspec +29 -0
- data/lib/epo/ops.rb +41 -0
- data/lib/epo/ops/address.rb +60 -0
- data/lib/epo/ops/bibliographic_document.rb +196 -0
- data/lib/epo/ops/client.rb +16 -0
- data/lib/epo/ops/error.rb +85 -0
- data/lib/epo/ops/limits.rb +22 -0
- data/lib/epo/ops/logger.rb +11 -0
- data/lib/epo/ops/rate_limit.rb +49 -0
- data/lib/epo/ops/register.rb +177 -0
- data/lib/epo/ops/search_query_builder.rb +70 -0
- data/lib/epo/ops/token_store.rb +29 -0
- data/lib/epo/ops/token_store/redis.rb +35 -0
- data/lib/epo/ops/util.rb +32 -0
- data/lib/epo/ops/version.rb +5 -0
- metadata +164 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7fdce426fc2fbb64f5f6c91f76f9892a0d9bcd59
|
4
|
+
data.tar.gz: ccc3a3ddd81354d151da39a4b37d01916fede709
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ef5f5150ab3eea7bb22675946a1f7c21c4fed46dcba1be39b187f633fe77a756aa261d4bc37a03e59b627005106a00450471da97b7eec5769c0435ed1ca13251
|
7
|
+
data.tar.gz: fb9e7f249f6330083d95ab565b2bb2824ea45f220b257fb52741ee76a0dc0165d493388ef2bfb29142e0c1aa4b0e45b76a59d726d96b1120cef49e8b29b6aceb
|
data/.gitignore
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/test/epo_credentials.yml
|
12
|
+
/tmp/
|
13
|
+
|
14
|
+
## Specific to RubyMotion:
|
15
|
+
.dat*
|
16
|
+
.repl_history
|
17
|
+
build/
|
18
|
+
|
19
|
+
## Documentation cache and generated files:
|
20
|
+
/.yardoc/
|
21
|
+
/_yardoc/
|
22
|
+
/doc/
|
23
|
+
/rdoc/
|
24
|
+
|
25
|
+
## Environment normalization:
|
26
|
+
/.bundle/
|
27
|
+
/vendor/bundle
|
28
|
+
/lib/bundler/man/
|
29
|
+
|
30
|
+
# for a library or gem, you might want to ignore these files since the code is
|
31
|
+
# intended to run in multiple environments; otherwise, check them in:
|
32
|
+
Gemfile.lock
|
33
|
+
# .ruby-version
|
34
|
+
# .ruby-gemset
|
35
|
+
|
36
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
37
|
+
.rvmrc
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 FHG-IMW
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
[](https://travis-ci.org/FHG-IMW/epo-ops)
|
2
|
+
[](https://codeclimate.com/github/FHG-IMW/epo-ops)
|
3
|
+
|
4
|
+
# epo-ops
|
5
|
+
Ruby interface to the EPO Open Patent Services (OPS).
|
6
|
+
|
7
|
+
You can play around with the API [here](https://developers.epo.org/).
|
8
|
+
Documentation of it can be found [here](https://www.epo.org/searching-for-patents/technical/espacenet/ops.html) under `Downloads`.
|
9
|
+
|
10
|
+
# Usage
|
11
|
+
|
12
|
+
## Authentification
|
13
|
+
In order to use this gem you need to register at the EPO for OAuth
|
14
|
+
[here](https://developers.epo.org/user/register).
|
15
|
+
Use your credentials by configuring
|
16
|
+
```ruby
|
17
|
+
Epo::Ops.configure do |conf|
|
18
|
+
conf.consumer_key = "YOUR_KEY"
|
19
|
+
conf.consumer_secret = "YOUR_SECRET"
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
## What works up to now
|
24
|
+
* Search the EPO OPS register with `Epo::Ops::Register.search(query)`; use `Epo::Ops::SearchQueryBuilder` to build an appropriate request.
|
25
|
+
* Get bibliographic info from the register, both for application and publication references (which you may retrieve with the search).
|
26
|
+
* Bulk searching for all patents on a given date wih `Epo::Ops::Register::Bulk.all_queries(date)`. Note that patents are usually published on Wednesdays, if you find some on another weekday, please let us know.
|
27
|
+
This method currently returns all queries necessary to find all patents with `Epo::Ops::Register.search`
|
28
|
+
|
29
|
+
### #search
|
30
|
+
Use the `SearchQueryBuilder` to set up the queries. By default structs are returned that should make it easier to work with the results, but with the `raw`-flag set to true you may also retrieve the resulting hash and parse it yourself.
|
31
|
+
The results have the method `#epodoc_reference` which perfectly fits into `#biblio`
|
32
|
+
|
33
|
+
### #biblio
|
34
|
+
With `Epo::Ops::Register.biblio(reference_id)` you can retrieve the bibliographic entry for the given patent (see OPS documentation). By default it searches the `/application/` endpoint, but you may set `publication` as the second parameter. Make sure the `reference_id` matches the given type. The last optional parameter allows you to set another format the id, but the default `epodoc` is strongly advised. This format is also provided from search results with `#epodoc_reference`.
|
data/Rakefile
ADDED
data/epo-ops.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'epo/ops/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'epo-ops'
|
8
|
+
spec.version = Epo::Ops::VERSION
|
9
|
+
spec.authors = ['Max Kießling', 'Robert Terbach', 'Michael Prilop']
|
10
|
+
|
11
|
+
spec.summary = 'Ruby interface to the European Patent Office API (OPS)'
|
12
|
+
spec.description = 'This gem allows simple access to the European Patent'\
|
13
|
+
' Offices (EPO) Open Patent Services (OPS) using their XML-API'
|
14
|
+
spec.homepage = 'https://github.com/FHG-IMW/epo-ops'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = 'exe'
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
22
|
+
spec.add_development_dependency 'rake', '~> 10.5'
|
23
|
+
spec.add_development_dependency 'minitest', '~> 5.8'
|
24
|
+
spec.add_development_dependency 'vcr', '~> 2.9'
|
25
|
+
spec.add_development_dependency 'webmock', '~> 1.22'
|
26
|
+
spec.add_development_dependency 'simplecov'
|
27
|
+
|
28
|
+
spec.add_dependency 'oauth2', '~> 1.1'
|
29
|
+
end
|
data/lib/epo/ops.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'epo/ops/version'
|
2
|
+
require 'epo/ops/token_store'
|
3
|
+
require 'epo/ops/register'
|
4
|
+
require 'epo/ops/search_query_builder'
|
5
|
+
|
6
|
+
module Epo
|
7
|
+
module Ops
|
8
|
+
# Configure your OAuth credentials to use with this gem.
|
9
|
+
# @example
|
10
|
+
# Epo:Ops.configure do |conf|
|
11
|
+
# conf.consumer_key = "foo"
|
12
|
+
# conf.consumer_secret = "bar"
|
13
|
+
# end
|
14
|
+
# Optional parameter:
|
15
|
+
# conf.token_store (defaults to {Epo::Ops::TokenStore})
|
16
|
+
# @yieldparam [Configuration] configuration that is yielded.
|
17
|
+
def self.configure
|
18
|
+
yield(config)
|
19
|
+
end
|
20
|
+
|
21
|
+
# The {Configuration} used. You may want to call {Epo::Ops#configure} first.
|
22
|
+
# @return [Configuration] the configuration used.
|
23
|
+
def self.config
|
24
|
+
@configuration ||= Configuration.new
|
25
|
+
end
|
26
|
+
|
27
|
+
class Configuration
|
28
|
+
attr_accessor :consumer_key, :consumer_secret, :token_store
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@consumer_key = ''
|
32
|
+
@consumer_secret = ''
|
33
|
+
@token_store = Epo::Ops::TokenStore.new
|
34
|
+
|
35
|
+
OAuth2::Response.register_parser(:xml, ['application/xml']) do |body|
|
36
|
+
MultiXml.parse(body)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Epo
|
2
|
+
module Ops
|
3
|
+
# Used to represent persons or companies (or both) in patents. Used for
|
4
|
+
# both, agents and applicants. Most of the time, when `name` is a person
|
5
|
+
# name, `address1` is a company name. Be aware that the addresses are in
|
6
|
+
# their respective local format.
|
7
|
+
#
|
8
|
+
# Current patents usually at least use the fields address1-3, so they should
|
9
|
+
# nearly always have values. Nevertheless, older ones often only use 1-2.
|
10
|
+
# Note also that EPOs schema documents fields like `street` or `city`, but
|
11
|
+
# by now they have not been used yet.
|
12
|
+
#
|
13
|
+
# @attr [String] name the name of an entity (one or more persons or
|
14
|
+
# companies)
|
15
|
+
# @attr [String] address1 first address line. May also be a company name
|
16
|
+
# @attr [String] address2 second address line
|
17
|
+
# @attr [String] address3 third address line, may be empty
|
18
|
+
# @attr [String] address4 fourth address line, may be empty
|
19
|
+
# @attr [String] address5 fifth address line, may be empty
|
20
|
+
# @attr [String] country_code two letter country code of the address
|
21
|
+
# @attr [Date] occurred_on the date an address occurred on, usually matching
|
22
|
+
# the entries change_gazette_num
|
23
|
+
# @attr [String] cdsid some kind of id the EPO provides, not sure yet if
|
24
|
+
# usable as reference.
|
25
|
+
class Address
|
26
|
+
attr_reader :name, :address1,
|
27
|
+
:address2,
|
28
|
+
:address3,
|
29
|
+
:address4,
|
30
|
+
:address5,
|
31
|
+
:country_code,
|
32
|
+
:occurred_on,
|
33
|
+
:cdsid
|
34
|
+
def initialize(name, address1, address2, address3, address4,
|
35
|
+
address5, country_code, occurred_on,
|
36
|
+
cdsid)
|
37
|
+
@address1 = address1
|
38
|
+
@address2 = address2
|
39
|
+
@address3 = address3 || ''
|
40
|
+
@address4 = address4 || ''
|
41
|
+
@address5 = address5 || ''
|
42
|
+
@name = name
|
43
|
+
@country_code = country_code || ''
|
44
|
+
@occurred_on = occurred_on || ''
|
45
|
+
@cdsid = cdsid || ''
|
46
|
+
end
|
47
|
+
|
48
|
+
# Compare addresses by the name and address fields.
|
49
|
+
# @return [Boolean]
|
50
|
+
def equal_name_and_address?(other)
|
51
|
+
name == other.name &&
|
52
|
+
address1 == other.address1 &&
|
53
|
+
address2 == other.address2 &&
|
54
|
+
address3 == other.address3 &&
|
55
|
+
address4 == other.address4 &&
|
56
|
+
address5 == other.address5
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'epo/ops/address'
|
2
|
+
|
3
|
+
module Epo
|
4
|
+
module Ops
|
5
|
+
# Parses and simplifies the elements the EPO OPS returns for bibliographic
|
6
|
+
# documents. Parsing is done lazily.
|
7
|
+
# Some elements are not yet fully parsed but hashes returned instead.
|
8
|
+
# Not all information available is parsed (e.g. inventors), if you need
|
9
|
+
# more fields, add them here.
|
10
|
+
class BibliographicDocument
|
11
|
+
# @return [Hash] a nested Hash, which is a parsed XML response of the
|
12
|
+
# `/biblio` endpoint of the EPO APIs.
|
13
|
+
# @see Client
|
14
|
+
attr_reader :raw
|
15
|
+
|
16
|
+
def initialize(raw)
|
17
|
+
@raw = raw
|
18
|
+
end
|
19
|
+
|
20
|
+
# A number by which a patent is uniquely identifiable and querieable.
|
21
|
+
# The first two letters are the country code of the processing patent
|
22
|
+
# office, for european patents this is EP.
|
23
|
+
# @return [String] application number.
|
24
|
+
def application_nr
|
25
|
+
@application_nr ||= parse_application_nr
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [String] The URL at which you can query the original document.
|
29
|
+
def url
|
30
|
+
@url ||= "https://ops.epo.org/3.1/rest-services/register/application/epodoc/#{application_nr}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [String] the english title of the patent @note Titles are
|
34
|
+
# usually available at least in english, french and german.
|
35
|
+
# Other languages are also possible.
|
36
|
+
def title
|
37
|
+
@title ||= parse_title
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Array] a list of the IPC-Classifications, as strings.
|
41
|
+
# Format is set by EPO, should be similar to: E06B7/23
|
42
|
+
def classifications
|
43
|
+
@classifications ||= parse_classification raw
|
44
|
+
end
|
45
|
+
|
46
|
+
# Agents and applicants are subject to change at EPO, often
|
47
|
+
# their names or addresses are updated, sometimes other
|
48
|
+
# people/companies appear or disappear.
|
49
|
+
#
|
50
|
+
# @return [Array] Array of {Address}
|
51
|
+
def agents
|
52
|
+
@agents ||= parse_agents raw
|
53
|
+
end
|
54
|
+
|
55
|
+
# (see #agents)
|
56
|
+
def applicants
|
57
|
+
@applicants ||= parse_applicants raw
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [String] the string representation of the current patent status as
|
61
|
+
# described by the EPO
|
62
|
+
def status
|
63
|
+
@status ||= parse_status raw
|
64
|
+
end
|
65
|
+
|
66
|
+
# Many fields of the XML the EPO provides have a field
|
67
|
+
# `change_gazette_num`. It is a commercial date (year + week)
|
68
|
+
# that describes in which week the element has been
|
69
|
+
# changed. This method parses them and returns the most recent
|
70
|
+
# date found.
|
71
|
+
# @return [Date] the latest date found in the document.
|
72
|
+
def latest_update
|
73
|
+
@latest ||= parse_latest_update raw
|
74
|
+
end
|
75
|
+
|
76
|
+
# The priority date describes the first document that was filed at any
|
77
|
+
# patent office in the world regarding this patent.
|
78
|
+
# @return [Hash] a hash which descibes the filed priority with the fields:
|
79
|
+
# `country` `doc_number`, `date`, `kind`, and `sequence`
|
80
|
+
def priority_date
|
81
|
+
@priority_date ||= parse_priority_date raw
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Array] List of hashes containing information about publications
|
85
|
+
# made, entries exist for multiple types of publications, e.g. A1, B1.
|
86
|
+
def publication_references
|
87
|
+
@publication_dates ||= parse_publication_references raw
|
88
|
+
end
|
89
|
+
|
90
|
+
def effective_date
|
91
|
+
@effective_date ||= parse_effective_date raw
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def parse_title
|
97
|
+
titles = Util.find_in_data(raw,
|
98
|
+
path_to_bibliographic_data +
|
99
|
+
['invention_title'])
|
100
|
+
titles.each do |the_title|
|
101
|
+
return the_title['__content__'] if the_title['lang'] == 'en'
|
102
|
+
end
|
103
|
+
# no english title found
|
104
|
+
titles.first['__content__']
|
105
|
+
end
|
106
|
+
|
107
|
+
def parse_application_nr
|
108
|
+
path = %w(world_patent_data register_search query __content__)
|
109
|
+
Util.find_in_data(raw, path).first.partition('=').last
|
110
|
+
end
|
111
|
+
|
112
|
+
def parse_priority_date(raw)
|
113
|
+
priority_claims = Util.find_in_data(raw,
|
114
|
+
path_to_bibliographic_data +
|
115
|
+
%w(priority_claims))
|
116
|
+
.first
|
117
|
+
if priority_claims.nil?
|
118
|
+
priority_date = nil
|
119
|
+
else
|
120
|
+
priority_date = priority_claims['priority_claim'].is_a?(Hash) ? priority_claims['priority_claim'] : priority_claims['priority_claim'].first
|
121
|
+
end
|
122
|
+
priority_date
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse_publication_references(raw)
|
126
|
+
Util.parse_hash_flat(
|
127
|
+
Util.find_in_data(raw,
|
128
|
+
path_to_bibliographic_data +
|
129
|
+
%w(publication_reference)), 'document_id')
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse_effective_date(raw)
|
133
|
+
effective_date =
|
134
|
+
Util.find_in_data(raw,
|
135
|
+
path_to_bibliographic_data +
|
136
|
+
%w(dates_rights_effective request_for_examination))
|
137
|
+
effective_date.first.nil? ? nil : effective_date.first['date']
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse_latest_update(raw)
|
141
|
+
gazette_nums = Util.parse_hash_flat(raw, 'change_gazette_num')
|
142
|
+
nums = gazette_nums.map { |num| Util.parse_change_gazette_num(num) }.keep_if { |match| !match.nil? }
|
143
|
+
nums.max
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_status(raw)
|
147
|
+
Util.find_in_data(raw,
|
148
|
+
path_to_bibliographic_data + ['status'])
|
149
|
+
.first
|
150
|
+
end
|
151
|
+
|
152
|
+
def parse_classification(raw)
|
153
|
+
Util.find_in_data(raw,
|
154
|
+
path_to_bibliographic_data +
|
155
|
+
%w(classifications_ipcr classification_ipcr text))
|
156
|
+
.first.split(',').map(&:strip)
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse_agents(raw)
|
160
|
+
entries = Util.find_in_data(raw,
|
161
|
+
path_to_bibliographic_data +
|
162
|
+
%w(parties agents))
|
163
|
+
parse_address(entries, 'agent')
|
164
|
+
end
|
165
|
+
|
166
|
+
def parse_applicants(raw)
|
167
|
+
entries = Util.find_in_data(raw,
|
168
|
+
path_to_bibliographic_data +
|
169
|
+
%w(parties applicants))
|
170
|
+
parse_address(entries, 'applicant')
|
171
|
+
end
|
172
|
+
|
173
|
+
def parse_address(party_group_entries, group)
|
174
|
+
party_group_entries.flat_map do |entry|
|
175
|
+
change_date = Util.parse_change_gazette_num(
|
176
|
+
entry.fetch('change_gazette_num', '')) || latest_update
|
177
|
+
Util.find_in_data(entry, [group, 'addressbook']).map do |address|
|
178
|
+
Address.new(address['name'],
|
179
|
+
address['address']['address_1'],
|
180
|
+
address['address']['address_2'],
|
181
|
+
address['address']['address_3'],
|
182
|
+
address['address']['address_4'],
|
183
|
+
address['address']['address_5'],
|
184
|
+
address['address']['country'],
|
185
|
+
change_date,
|
186
|
+
address['cdsid'])
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def path_to_bibliographic_data
|
192
|
+
%w(world_patent_data register_search register_documents register_document bibliographic_data)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'epo/ops/token_store'
|
2
|
+
require 'epo/ops/error'
|
3
|
+
|
4
|
+
module Epo
|
5
|
+
module Ops
|
6
|
+
class Client
|
7
|
+
# @return [OAuth2::Response]
|
8
|
+
def self.request(verb, url, options = {})
|
9
|
+
token = Epo::Ops.config.token_store.token
|
10
|
+
response = token.request(verb, URI.encode(url), options)
|
11
|
+
fail Error.from_response(response) unless response.status == 200
|
12
|
+
response
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'epo/ops/rate_limit'
|
2
|
+
|
3
|
+
module Epo
|
4
|
+
module Ops
|
5
|
+
class Error < StandardError
|
6
|
+
# @return [Integer]
|
7
|
+
attr_reader :code, :rate_limit
|
8
|
+
|
9
|
+
# Raised when EPO returns a 4xx HTTP status code
|
10
|
+
ClientError = Class.new(self)
|
11
|
+
# Raised when EPO returns the HTTP status code 400
|
12
|
+
BadRequest = Class.new(ClientError)
|
13
|
+
# Raised when EPO returns the HTTP status code 401
|
14
|
+
Unauthorized = Class.new(ClientError)
|
15
|
+
# Raised when EPO returns the HTTP status code 403
|
16
|
+
Forbidden = Class.new(ClientError)
|
17
|
+
# Raised when EPO returns the HTTP status code 404
|
18
|
+
NotFound = Class.new(ClientError)
|
19
|
+
# Raised when EPO returns the HTTP status code 406
|
20
|
+
NotAcceptable = Class.new(ClientError)
|
21
|
+
# Raised when EPO returns the HTTP status code 422
|
22
|
+
UnprocessableEntity = Class.new(ClientError)
|
23
|
+
# Raised when EPO returns the HTTP status code 429
|
24
|
+
TooManyRequests = Class.new(ClientError)
|
25
|
+
# Raised when EPO returns a 5xx HTTP status code
|
26
|
+
ServerError = Class.new(self)
|
27
|
+
# Raised when EPO returns the HTTP status code 500
|
28
|
+
InternalServerError = Class.new(ServerError)
|
29
|
+
# Raised when EPO returns the HTTP status code 502
|
30
|
+
BadGateway = Class.new(ServerError)
|
31
|
+
# Raised when EPO returns the HTTP status code 503
|
32
|
+
ServiceUnavailable = Class.new(ServerError)
|
33
|
+
# Raised when EPO returns the HTTP status code 504
|
34
|
+
GatewayTimeout = Class.new(ServerError)
|
35
|
+
|
36
|
+
ERRORS = {
|
37
|
+
400 => Epo::Ops::Error::BadRequest,
|
38
|
+
401 => Epo::Ops::Error::Unauthorized,
|
39
|
+
403 => Epo::Ops::Error::Forbidden,
|
40
|
+
404 => Epo::Ops::Error::NotFound,
|
41
|
+
406 => Epo::Ops::Error::NotAcceptable,
|
42
|
+
422 => Epo::Ops::Error::UnprocessableEntity,
|
43
|
+
429 => Epo::Ops::Error::TooManyRequests,
|
44
|
+
500 => Epo::Ops::Error::InternalServerError,
|
45
|
+
502 => Epo::Ops::Error::BadGateway,
|
46
|
+
503 => Epo::Ops::Error::ServiceUnavailable,
|
47
|
+
504 => Epo::Ops::Error::GatewayTimeout
|
48
|
+
}.freeze
|
49
|
+
FORBIDDEN_MESSAGES = {
|
50
|
+
'This request has been rejected due to the violation of Fair Use policy' => Epo::Ops::Error::TooManyRequests
|
51
|
+
}.freeze
|
52
|
+
|
53
|
+
class << self
|
54
|
+
# Parses an error from the given response
|
55
|
+
# @return [Error]
|
56
|
+
def from_response(response)
|
57
|
+
code = response.status
|
58
|
+
message = parse_error(response.parsed)
|
59
|
+
|
60
|
+
if code == 403 && FORBIDDEN_MESSAGES[message]
|
61
|
+
FORBIDDEN_MESSAGES[message].new(message, response.headers, code)
|
62
|
+
else
|
63
|
+
ERRORS[code].new(message, response.headers, code)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def parse_error(body)
|
70
|
+
if body.nil? || body.empty?
|
71
|
+
nil
|
72
|
+
elsif body['error'] && body['error']['message']
|
73
|
+
body['error']['message']
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def initialize(message = '', rate_limit = {}, code = nil)
|
79
|
+
super(message)
|
80
|
+
@code = code
|
81
|
+
@rate_limit = RateLimit.new(rate_limit)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Epo
|
2
|
+
module Ops
|
3
|
+
# The register search is limited by some parameters. With one
|
4
|
+
# query one may only request as many as
|
5
|
+
# {Epo::Ops::Limits::MAX_QUERY_INTERVAL} references at once.
|
6
|
+
# Considering this, you have to split your requests by this
|
7
|
+
# interval. Nevertheless, the maximum value you may use is
|
8
|
+
# {Epo::Ops::Limits::MAX_QUERY_RANGE}. If you want to retrieve more
|
9
|
+
# references you must split by other parameters.
|
10
|
+
# @see Register
|
11
|
+
class Limits
|
12
|
+
# @return [Integer] The range in which you can search is limited, say you
|
13
|
+
# cannot request all patents of a given class at once, you probably must
|
14
|
+
# split your requests by additional conditions.
|
15
|
+
MAX_QUERY_RANGE = 2000
|
16
|
+
|
17
|
+
# @return [Integer] The maximum number of elements you may search with one
|
18
|
+
# query. Ignoring this will result in errors.
|
19
|
+
MAX_QUERY_INTERVAL = 100
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Epo
|
2
|
+
module Ops
|
3
|
+
class RateLimit
|
4
|
+
WEEKLY_QUOTA_RESET_TIME = 604_800
|
5
|
+
HOURLY_QUOTA_RESET_TIME = 600
|
6
|
+
BASE_RESET_TIME = 60
|
7
|
+
|
8
|
+
attr_reader :attr
|
9
|
+
|
10
|
+
def initialize(http_header)
|
11
|
+
fail "Rate Limit data should be a Hash but is #{http_header.inspect} (#{http_header.class.name})" unless http_header.is_a?(Hash)
|
12
|
+
@attr = http_header
|
13
|
+
end
|
14
|
+
|
15
|
+
def limit_reached?
|
16
|
+
@attr.key?('x-rejection-reason')
|
17
|
+
end
|
18
|
+
|
19
|
+
def rejection_reason
|
20
|
+
return nil unless @attr['x-rejection-reason']
|
21
|
+
case @attr['x-rejection-reason']
|
22
|
+
when 'RegisteredQuotaPerWeek' then :weekly_quota
|
23
|
+
when 'IndividualQuotaPerHour' then :hourly_quota
|
24
|
+
else :unknown_reason
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def hourly_quota
|
29
|
+
quota = @attr['x-individualquotaperhour-used']
|
30
|
+
quota.to_i if quota
|
31
|
+
end
|
32
|
+
|
33
|
+
def weekly_quota
|
34
|
+
quota = @attr['x-registeredquotaperweek-used']
|
35
|
+
quota.to_i if quota
|
36
|
+
end
|
37
|
+
|
38
|
+
def reset_at
|
39
|
+
return unless limit_reached?
|
40
|
+
|
41
|
+
case rejection_reason
|
42
|
+
when :weekly_quota then Time.now.to_i + WEEKLY_QUOTA_RESET_TIME
|
43
|
+
when :hourly_quota then Time.now.to_i + HOURLY_QUOTA_RESET_TIME
|
44
|
+
else Time.now.to_i + BASE_RESET_TIME
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'epo/ops'
|
2
|
+
require 'epo/ops/client'
|
3
|
+
require 'epo/ops/util'
|
4
|
+
require 'epo/ops/bibliographic_document'
|
5
|
+
require 'epo/ops/logger'
|
6
|
+
|
7
|
+
module Epo
|
8
|
+
module Ops
|
9
|
+
# Access to the {http://ops.epo.org/3.1/rest-services/register register}
|
10
|
+
# endpoint of the EPO OPS API.
|
11
|
+
#
|
12
|
+
# By now you can search and retrieve patents by using the type `application`
|
13
|
+
# in the `epodoc` format.
|
14
|
+
#
|
15
|
+
# Search queries are limited by size, not following these limits
|
16
|
+
# will result in errors.
|
17
|
+
#
|
18
|
+
# @see Limits
|
19
|
+
# @see SearchQueryBuilder
|
20
|
+
class Register
|
21
|
+
# Helper class that assists in building the queries necessary to search
|
22
|
+
# for more patents than possible with one query respecting the given
|
23
|
+
# limits.
|
24
|
+
#
|
25
|
+
# @see Limits
|
26
|
+
class Bulk
|
27
|
+
# Helper method returning all unique register references on a given
|
28
|
+
# date. This is the same as executing all queries from {.all_queries}
|
29
|
+
# and making the results unique.
|
30
|
+
#
|
31
|
+
# @note Patents may have more than one IPC class, they would appear
|
32
|
+
# more than once, this method filters these by `doc_number`
|
33
|
+
def self.all_register_references(date)
|
34
|
+
begin
|
35
|
+
queries = Bulk.all_queries(date)
|
36
|
+
search_entries = queries.flat_map do |query|
|
37
|
+
Register.search(query)
|
38
|
+
end
|
39
|
+
rescue ::Epo::Ops::Error::NotFound
|
40
|
+
return []
|
41
|
+
end
|
42
|
+
search_entries.map(&:application_reference)
|
43
|
+
.uniq(&:doc_number)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Build the queries to search for all patents on a given date.
|
47
|
+
#
|
48
|
+
# The offset of EPOs register search may at max be 2000, if more patents
|
49
|
+
# are published on one day the queries must be split; here across the
|
50
|
+
# first level of ipc classification.
|
51
|
+
# At time of this writing they are mostly below 1000, there should be
|
52
|
+
# plenty of space for now.
|
53
|
+
#
|
54
|
+
# In case the limits change, they can be found in {Epo::Ops::Limits}
|
55
|
+
# Should there be more than 2000 patents in one class, a message will
|
56
|
+
# be logged, please file an Issue if that happens.
|
57
|
+
#
|
58
|
+
# @return [Array] containing all queries to put into {Register.search}.
|
59
|
+
# @note The queries are split by IPC-classes if necessary; Patents may
|
60
|
+
# have more than one, you might get multiple references to the same
|
61
|
+
# patent.
|
62
|
+
# @see .all_register_references
|
63
|
+
def self.all_queries(date)
|
64
|
+
overall_count = published_patents_count(date)
|
65
|
+
if overall_count > Limits::MAX_QUERY_RANGE
|
66
|
+
patent_count_by_ipc_classes(date).flat_map do |ipc_class, count|
|
67
|
+
builder = SearchQueryBuilder.new
|
68
|
+
.publication_date(date.year, date.month, date.day)
|
69
|
+
.and
|
70
|
+
.ipc_class(ipc_class)
|
71
|
+
split_by_size_limits(builder, count)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
builder = SearchQueryBuilder.new
|
75
|
+
.publication_date(date.year, date.month, date.day)
|
76
|
+
split_by_size_limits(builder, overall_count)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Hash] For all top level IPC classes (A-H) => count
|
81
|
+
def self.patent_count_by_ipc_classes(date)
|
82
|
+
ipc_classes = %w(A B C D E F G H)
|
83
|
+
ipc_classes.inject({}) do |mem, ipcc|
|
84
|
+
mem[ipcc] = published_patents_count(date, ipcc)
|
85
|
+
if mem[ipcc] > Limits::MAX_QUERY_RANGE
|
86
|
+
Logger.log("IPC class #{ipcc} has more than #{Epo::Ops::Limits::MAX_QUERY_RANGE} on #{date}. They can not all be retrieved. Please file this as an issue!")
|
87
|
+
end
|
88
|
+
mem
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Splits the queries build by `query_builder` by the allowed intervals.
|
93
|
+
#
|
94
|
+
# @param query_builder [SearchQueryBuilder] with all settings made, but
|
95
|
+
# not built yet.
|
96
|
+
# @param patent_count [Integer] number of overall results expected.
|
97
|
+
# See {.published_patents_count}
|
98
|
+
#
|
99
|
+
# @return [Array] of Strings, each a query to put into {Register.search}
|
100
|
+
def self.split_by_size_limits(query_builder, patent_count)
|
101
|
+
max_interval = Limits::MAX_QUERY_INTERVAL
|
102
|
+
(1..patent_count).step(max_interval).map do |start|
|
103
|
+
query_builder.build(start, [start + max_interval - 1, patent_count].min)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# makes a minimum request to find out how many patents are published on
|
108
|
+
# that date
|
109
|
+
#
|
110
|
+
# @return [Integer] number of patents on that date.
|
111
|
+
def self.published_patents_count(date, ipc_class = nil)
|
112
|
+
query = SearchQueryBuilder.new
|
113
|
+
query.publication_date(date.year, date.month, date.day)
|
114
|
+
query.and.ipc_class(ipc_class) if ipc_class
|
115
|
+
query = query.build(1, 2)
|
116
|
+
minimum_result_set = Register.search(query, true)
|
117
|
+
return 0 if minimum_result_set.empty?
|
118
|
+
minimum_result_set['world_patent_data']['register_search']['total_result_count'].to_i
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# @param query A query built with {Epo::Ops::SearchQueryBuilder}
|
123
|
+
# @param raw if `true` the result will be the raw response as a nested hash.
|
124
|
+
# if false(default) the result will be parsed further, returning a list of [SearchEntry]
|
125
|
+
# @return [Array] containing {SearchEntry}
|
126
|
+
def self.search(query, raw = false)
|
127
|
+
hash = Client.request(:get, register_api_string + query).parsed
|
128
|
+
return parse_search_results(hash) unless raw
|
129
|
+
hash
|
130
|
+
end
|
131
|
+
|
132
|
+
# @param format epodoc is a format defined by the EPO for a
|
133
|
+
# document id. see their documentation.
|
134
|
+
# @param type may be `application` or `publication` make sure that the
|
135
|
+
# `reference_id` is matching
|
136
|
+
# @param raw flag if the result should be returned as a raw Hash or
|
137
|
+
# parsed as {BibliographicDocument}
|
138
|
+
# @return [BibliographicDocument, Hash]
|
139
|
+
def self.biblio(reference_id, type = 'application', format = 'epodoc', raw = false)
|
140
|
+
request = "#{register_api_string}#{type}/#{format}/#{reference_id}/biblio"
|
141
|
+
result = Client.request(:get, request).parsed
|
142
|
+
raw ? result : BibliographicDocument.new(result)
|
143
|
+
end
|
144
|
+
|
145
|
+
Reference = Struct.new(:country, :doc_number, :date) do
|
146
|
+
def epodoc_reference
|
147
|
+
country + doc_number
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
SearchEntry = Struct.new(:publication_reference, :application_reference, :ipc_classes)
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def self.parse_search_results(result)
|
156
|
+
path = %w(world_patent_data register_search register_documents register_document bibliographic_data)
|
157
|
+
|
158
|
+
list = Util.find_in_data(result, path)
|
159
|
+
list.map do |entry|
|
160
|
+
publication_reference = Reference.new(
|
161
|
+
entry['publication_reference']['document_id']['country'],
|
162
|
+
entry['publication_reference']['document_id']['doc_number'],
|
163
|
+
entry['publication_reference']['document_id']['date'])
|
164
|
+
application_reference = Reference.new(
|
165
|
+
entry['application_reference']['document_id']['country'],
|
166
|
+
entry['application_reference']['document_id']['doc_number'])
|
167
|
+
ipc_classes = entry['classifications_ipcr']['classification_ipcr']['text'].split(';;').map(&:strip)
|
168
|
+
SearchEntry.new(publication_reference, application_reference, ipc_classes)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.register_api_string
|
173
|
+
'/3.1/rest-services/register/'
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'epo/ops/limits'
|
2
|
+
require 'epo/ops/logger'
|
3
|
+
|
4
|
+
module Epo
|
5
|
+
module Ops
|
6
|
+
# This Builder helps creating a search query using
|
7
|
+
# {https://www.loc.gov/standards/sru/cql/ CQL} (Common Query Language or
|
8
|
+
# Contextual Query Language) with the identifies specified by the EPO in
|
9
|
+
# the OPS Documentation chapter 4.2 ( {https://www.epo.org/searching-for-patents/technical/espacenet/ops.html Link}
|
10
|
+
# - use tab Downloads and see file 'OPS version 3.1 documentation').
|
11
|
+
# Dont use a builder twice ;)
|
12
|
+
class SearchQueryBuilder
|
13
|
+
def initialize
|
14
|
+
@query = 'search?q='
|
15
|
+
end
|
16
|
+
|
17
|
+
def publication_date(year, month, day)
|
18
|
+
@query << "pd=#{('%04d' % year) << ('%02d' % month) << ('%02d' % day)}"
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def and
|
23
|
+
@query << ' and '
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def ipc_class(ipc_class)
|
28
|
+
@query << "ic=#{ipc_class}"
|
29
|
+
# TODO: ipc_class richtig formatieren
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
# builds the search query ready to put into the register API. The
|
34
|
+
# parameters are validated with {#validate_range}.
|
35
|
+
# This does not change the query, several calls will allow you to
|
36
|
+
# create the same queries for different ranges.
|
37
|
+
def build(range_start = 1, range_end = nil)
|
38
|
+
range_end ||= range_start + Limits::MAX_QUERY_INTERVAL - 1
|
39
|
+
validated_range = validate_range range_start, range_end
|
40
|
+
@query + "&Range=#{validated_range[0]}-#{validated_range[1]}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Fixes the range given so that they meed the EPO APIs rules. The range
|
44
|
+
# may only be 100 elements long, the maximum allowed value is 2000.
|
45
|
+
# If the given window is out of range, it will be moved preserving the
|
46
|
+
# distance covered.
|
47
|
+
# @see Epo::Ops::Limits
|
48
|
+
# @return array with two elements: [range_start, range_end]
|
49
|
+
def validate_range(range_start, range_end)
|
50
|
+
if range_start > range_end
|
51
|
+
range_start, range_end = range_end, range_start
|
52
|
+
Logger.log('range_start was bigger than range_end, swapped values')
|
53
|
+
elsif range_start == range_end || range_end - range_start > Limits::MAX_QUERY_INTERVAL - 1
|
54
|
+
range_end = range_start + Limits::MAX_QUERY_INTERVAL - 1
|
55
|
+
Logger.log("range invalid, set to: #{[range_start, range_end]}")
|
56
|
+
end
|
57
|
+
if range_start < 1
|
58
|
+
range_end = range_end - range_start + 1
|
59
|
+
range_start = 1
|
60
|
+
Logger.log("range_start must be > 0, set to: #{[range_start, range_end]}")
|
61
|
+
elsif range_end > Limits::MAX_QUERY_RANGE
|
62
|
+
range_start = Limits::MAX_QUERY_RANGE - (range_end - range_start)
|
63
|
+
range_end = Limits::MAX_QUERY_RANGE
|
64
|
+
Logger.log("range_end was too big, set to: #{[range_start, range_end]}")
|
65
|
+
end
|
66
|
+
[range_start, range_end]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'oauth2'
|
2
|
+
require 'epo/ops'
|
3
|
+
|
4
|
+
module Epo
|
5
|
+
module Ops
|
6
|
+
# This class saves the token in memory, you may want to subclass this and
|
7
|
+
# overwrite #token if you want to store it somewhere else.
|
8
|
+
#
|
9
|
+
class TokenStore
|
10
|
+
def token
|
11
|
+
return generate_token if !@token || @token.expired?
|
12
|
+
@token
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def generate_token
|
18
|
+
client = OAuth2::Client.new(
|
19
|
+
Epo::Ops.config.consumer_key,
|
20
|
+
Epo::Ops.config.consumer_secret,
|
21
|
+
site: 'https://ops.epo.org/',
|
22
|
+
token_url: '/3.1/auth/accesstoken',
|
23
|
+
raise_errors: false
|
24
|
+
)
|
25
|
+
@token = client.client_credentials.get_token
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'connection_pool'
|
3
|
+
|
4
|
+
module Epo
|
5
|
+
module Ops
|
6
|
+
class TokenStore
|
7
|
+
class Redis < TokenStore
|
8
|
+
def initialize(redis_host)
|
9
|
+
fail "Please install gems 'redis' and 'connection_pool' to use this feature" unless defined?(::Redis) && defined?(ConnectionPool)
|
10
|
+
|
11
|
+
@redis = ConnectionPool.new(size: 5, timeout: 5) { ::Redis.new(host: redis_host) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def token
|
15
|
+
token = nil
|
16
|
+
@redis.conn do |conn|
|
17
|
+
token = conn.get("epo_token_#{id}")
|
18
|
+
end
|
19
|
+
|
20
|
+
token.present? ? OAuth2::AccessToken.new(client, token) : generate_token
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def generate_token
|
26
|
+
super
|
27
|
+
Sidekiq.redis do |conn|
|
28
|
+
conn.set("epo_token_#{id}", token.token, ex: token.expires_in, nx: true)
|
29
|
+
end
|
30
|
+
token
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/epo/ops/util.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Epo
|
2
|
+
module Ops
|
3
|
+
class Util
|
4
|
+
# the path should be an array of strings indicating the path you want to go in the hash
|
5
|
+
def self.find_in_data(epo_hash, path)
|
6
|
+
path.reduce(epo_hash) { |res, c| parse_hash_flat(res, c) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.parse_hash_flat(hash_layer, target)
|
10
|
+
result = []
|
11
|
+
if hash_layer.nil?
|
12
|
+
return []
|
13
|
+
elsif hash_layer.class == String
|
14
|
+
return []
|
15
|
+
elsif hash_layer.class == Array
|
16
|
+
result.concat(hash_layer.map { |x| parse_hash_flat(x, target) })
|
17
|
+
elsif hash_layer[target]
|
18
|
+
result << hash_layer[target]
|
19
|
+
elsif hash_layer.class == Hash || hash_layer.respond_to?(:to_h)
|
20
|
+
result.concat(hash_layer.to_h.map { |_x, y| parse_hash_flat(y, target) })
|
21
|
+
end
|
22
|
+
result.flatten
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.parse_change_gazette_num(num)
|
26
|
+
res = /^(?<year>\d{4})\/(?<week>\d{2})$/.match(num)
|
27
|
+
return nil if res.nil?
|
28
|
+
Date.commercial(Integer(res[:year], 10), week = Integer(res[:week], 10))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: epo-ops
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Max Kießling
|
8
|
+
- Robert Terbach
|
9
|
+
- Michael Prilop
|
10
|
+
autorequire:
|
11
|
+
bindir: exe
|
12
|
+
cert_chain: []
|
13
|
+
date: 2016-03-01 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: bundler
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '1.3'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: rake
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ~>
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '10.5'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ~>
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '10.5'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: minitest
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ~>
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '5.8'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ~>
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '5.8'
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: vcr
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ~>
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '2.9'
|
64
|
+
type: :development
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ~>
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '2.9'
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: webmock
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '1.22'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ~>
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '1.22'
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: simplecov
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
- !ruby/object:Gem::Dependency
|
100
|
+
name: oauth2
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ~>
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '1.1'
|
106
|
+
type: :runtime
|
107
|
+
prerelease: false
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ~>
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '1.1'
|
113
|
+
description: This gem allows simple access to the European Patent Offices (EPO) Open
|
114
|
+
Patent Services (OPS) using their XML-API
|
115
|
+
email:
|
116
|
+
executables: []
|
117
|
+
extensions: []
|
118
|
+
extra_rdoc_files: []
|
119
|
+
files:
|
120
|
+
- .gitignore
|
121
|
+
- .travis.yml
|
122
|
+
- Gemfile
|
123
|
+
- LICENSE
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- epo-ops.gemspec
|
127
|
+
- lib/epo/ops.rb
|
128
|
+
- lib/epo/ops/address.rb
|
129
|
+
- lib/epo/ops/bibliographic_document.rb
|
130
|
+
- lib/epo/ops/client.rb
|
131
|
+
- lib/epo/ops/error.rb
|
132
|
+
- lib/epo/ops/limits.rb
|
133
|
+
- lib/epo/ops/logger.rb
|
134
|
+
- lib/epo/ops/rate_limit.rb
|
135
|
+
- lib/epo/ops/register.rb
|
136
|
+
- lib/epo/ops/search_query_builder.rb
|
137
|
+
- lib/epo/ops/token_store.rb
|
138
|
+
- lib/epo/ops/token_store/redis.rb
|
139
|
+
- lib/epo/ops/util.rb
|
140
|
+
- lib/epo/ops/version.rb
|
141
|
+
homepage: https://github.com/FHG-IMW/epo-ops
|
142
|
+
licenses: []
|
143
|
+
metadata: {}
|
144
|
+
post_install_message:
|
145
|
+
rdoc_options: []
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
requirements: []
|
159
|
+
rubyforge_project:
|
160
|
+
rubygems_version: 2.4.6
|
161
|
+
signing_key:
|
162
|
+
specification_version: 4
|
163
|
+
summary: Ruby interface to the European Patent Office API (OPS)
|
164
|
+
test_files: []
|