epo-ops 0.2.6 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis.yml +6 -0
  4. data/README.md +78 -38
  5. data/epo-ops.gemspec +2 -2
  6. data/lib/epo_ops.rb +46 -0
  7. data/lib/epo_ops/client.rb +46 -0
  8. data/lib/epo_ops/error.rb +87 -0
  9. data/lib/epo_ops/factories.rb +9 -0
  10. data/lib/epo_ops/factories/name_and_address_factory.rb +54 -0
  11. data/lib/epo_ops/factories/patent_application_factory.rb +116 -0
  12. data/lib/epo_ops/factories/register_search_result_factory.rb +42 -0
  13. data/lib/epo_ops/ipc_class_hierarchy.rb +146 -0
  14. data/lib/epo_ops/ipc_class_hierarchy_loader.rb +60 -0
  15. data/lib/epo_ops/ipc_class_util.rb +71 -0
  16. data/lib/epo_ops/limits.rb +20 -0
  17. data/lib/epo_ops/logger.rb +15 -0
  18. data/lib/epo_ops/name_and_address.rb +58 -0
  19. data/lib/epo_ops/patent_application.rb +159 -0
  20. data/lib/epo_ops/rate_limit.rb +47 -0
  21. data/lib/epo_ops/register.rb +100 -0
  22. data/lib/epo_ops/register_search_result.rb +40 -0
  23. data/lib/epo_ops/search_query_builder.rb +65 -0
  24. data/lib/epo_ops/token_store.rb +33 -0
  25. data/lib/epo_ops/token_store/redis.rb +45 -0
  26. data/lib/epo_ops/util.rb +52 -0
  27. data/lib/epo_ops/version.rb +3 -0
  28. metadata +26 -20
  29. data/lib/epo/ops.rb +0 -43
  30. data/lib/epo/ops/address.rb +0 -60
  31. data/lib/epo/ops/bibliographic_document.rb +0 -196
  32. data/lib/epo/ops/client.rb +0 -27
  33. data/lib/epo/ops/error.rb +0 -89
  34. data/lib/epo/ops/ipc_class_hierarchy.rb +0 -148
  35. data/lib/epo/ops/ipc_class_hierarchy_loader.rb +0 -62
  36. data/lib/epo/ops/ipc_class_util.rb +0 -73
  37. data/lib/epo/ops/limits.rb +0 -22
  38. data/lib/epo/ops/logger.rb +0 -11
  39. data/lib/epo/ops/rate_limit.rb +0 -49
  40. data/lib/epo/ops/register.rb +0 -152
  41. data/lib/epo/ops/search_query_builder.rb +0 -65
  42. data/lib/epo/ops/token_store.rb +0 -35
  43. data/lib/epo/ops/token_store/redis.rb +0 -47
  44. data/lib/epo/ops/util.rb +0 -32
  45. data/lib/epo/ops/version.rb +0 -6
@@ -0,0 +1,42 @@
1
+ module EpoOps
2
+ module Factories
3
+ # Parses the register search result from EPO Ops into an RegisterSearchResult object
4
+ class RegisterSearchResultFactory
5
+ class << self
6
+
7
+ # @param raw_data [Hash] raw search result as retrieved from Epo Ops
8
+ # @return [EpoOps::RegisterSearchResult] RegisterSearchResult filled with parsed data
9
+ def build(raw_data)
10
+ factory = new(raw_data)
11
+
12
+ EpoOps::RegisterSearchResult.new(
13
+ factory.patents,
14
+ factory.count,
15
+ factory.raw_data
16
+ )
17
+ end
18
+ end
19
+
20
+ attr_reader :raw_data
21
+
22
+ def initialize(raw_data)
23
+ @raw_data = raw_data
24
+ end
25
+
26
+ # @return [integer] The number of applications matching the query
27
+ # @see EpoOps::RegisterSearchResult#count
28
+ def count
29
+ EpoOps::Util.dig(@raw_data, 'world_patent_data', 'register_search', 'total_result_count').to_i
30
+ end
31
+
32
+ # @return [Array] the patents returned by the search. Patentapplication data is not complete
33
+ # @see EpoOps::RegisterSearchResult#patents
34
+ def patents
35
+ EpoOps::Util.flat_dig(
36
+ @raw_data,
37
+ %w(world_patent_data register_search register_documents register_document)
38
+ ).map {|patent_data| EpoOps::Factories::PatentApplicationFactory.build(patent_data)}
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,146 @@
1
+ module EpoOps
2
+ # The hierarchy is a flat Hash, that helps finding all known ipc subclasses
3
+ # of a given class. It was parsed from the WIPO. It does not support all
4
+ # levels, as it would (currently unnecessarily) blow up this hash. It only
5
+ # finds the first two sub class levels, e.g. A45F.
6
+ class IpcClassHierarchy
7
+ Hierarchy = { 'A' => %w(A01 A21 A22 A23 A24 A41 A42 A43 A44 A45 A46 A47 A61 A62 A63 A99),
8
+ 'A01' => %w(A01B A01C A01D A01F A01G A01H A01J A01K A01L A01M A01N A01P),
9
+ 'A21' => %w(A21B A21C A21D),
10
+ 'A22' => %w(A22B A22C),
11
+ 'A23' => %w(A23B A23C A23D A23F A23G A23J A23K A23L A23N A23P),
12
+ 'A24' => %w(A24B A24C A24D A24F),
13
+ 'A41' => %w(A41B A41C A41D A41F A41G A41H),
14
+ 'A42' => %w(A42B A42C),
15
+ 'A43' => %w(A43B A43C A43D),
16
+ 'A44' => %w(A44B A44C),
17
+ 'A45' => %w(A45B A45C A45D A45F),
18
+ 'A46' => %w(A46B A46D),
19
+ 'A47' => %w(A47B A47C A47D A47F A47G A47H A47J A47K A47L),
20
+ 'A61' => %w(A61B A61C A61D A61F A61G A61H A61J A61K A61L A61M A61N A61P A61Q),
21
+ 'A62' => %w(A62B A62C A62D),
22
+ 'A63' => %w(A63B A63C A63D A63F A63G A63H A63J A63K),
23
+ 'A99' => ['A99Z'],
24
+ 'B' => %w(B01 B02 B03 B04 B05 B06 B07 B08 B09 B21 B22 B23 B24 B25 B26 B27 B28 B29 B30 B31 B32 B33 B41 B42 B43 B44 B60 B61 B62 B63 B64 B65 B66 B67 B68 B81 B82 B99),
25
+ 'B01' => %w(B01B B01D B01F B01J B01L),
26
+ 'B02' => %w(B02B B02C),
27
+ 'B03' => %w(B03B B03C B03D),
28
+ 'B04' => %w(B04B B04C),
29
+ 'B05' => %w(B05B B05C B05D),
30
+ 'B06' => ['B06B'],
31
+ 'B07' => %w(B07B B07C),
32
+ 'B08' => ['B08B'],
33
+ 'B09' => %w(B09B B09C),
34
+ 'B21' => %w(B21B B21C B21D B21F B21G B21H B21J B21K B21L),
35
+ 'B22' => %w(B22C B22D B22F),
36
+ 'B23' => %w(B23B B23C B23D B23F B23G B23H B23K B23P B23Q),
37
+ 'B24' => %w(B24B B24C B24D),
38
+ 'B25' => %w(B25B B25C B25D B25F B25G B25H B25J),
39
+ 'B26' => %w(B26B B26D B26F),
40
+ 'B27' => %w(B27B B27C B27D B27F B27G B27H B27J B27K B27L B27M B27N),
41
+ 'B28' => %w(B28B B28C B28D),
42
+ 'B29' => %w(B29B B29C B29D B29K B29L),
43
+ 'B30' => ['B30B'],
44
+ 'B31' => %w(B31B B31C B31D B31F),
45
+ 'B32' => ['B32B'],
46
+ 'B33' => ['B33Y'],
47
+ 'B41' => %w(B41B B41C B41D B41F B41G B41J B41K B41L B41M B41N),
48
+ 'B42' => %w(B42B B42C B42D B42F),
49
+ 'B43' => %w(B43K B43L B43M),
50
+ 'B44' => %w(B44B B44C B44D B44F),
51
+ 'B60' => %w(B60B B60C B60D B60F B60G B60H B60J B60K B60L B60M B60N B60P B60Q B60R B60S B60T B60V B60W),
52
+ 'B61' => %w(B61B B61C B61D B61F B61G B61H B61J B61K B61L),
53
+ 'B62' => %w(B62B B62C B62D B62H B62J B62K B62L B62M),
54
+ 'B63' => %w(B63B B63C B63G B63H B63J),
55
+ 'B64' => %w(B64B B64C B64D B64F B64G),
56
+ 'B65' => %w(B65B B65C B65D B65F B65G B65H),
57
+ 'B66' => %w(B66B B66C B66D B66F),
58
+ 'B67' => %w(B67B B67C B67D),
59
+ 'B68' => %w(B68B B68C B68F B68G),
60
+ 'B81' => %w(B81B B81C),
61
+ 'B82' => %w(B82B B82Y),
62
+ 'B99' => ['B99Z'],
63
+ 'C' => %w(C01 C02 C03 C04 C05 C06 C07 C08 C09 C10 C11 C12 C13 C14 C21 C22 C23 C25 C30 C40 C99),
64
+ 'C01' => %w(C01B C01C C01D C01F C01G),
65
+ 'C02' => ['C02F'],
66
+ 'C03' => %w(C03B C03C),
67
+ 'C04' => ['C04B'],
68
+ 'C05' => %w(C05B C05C C05D C05F C05G),
69
+ 'C06' => %w(C06B C06C C06D C06F),
70
+ 'C07' => %w(C07B C07C C07D C07F C07G C07H C07J C07K),
71
+ 'C08' => %w(C08B C08C C08F C08G C08H C08J C08K C08L),
72
+ 'C09' => %w(C09B C09C C09D C09F C09G C09H C09J C09K),
73
+ 'C10' => %w(C10B C10C C10F C10G C10H C10J C10K C10L C10M C10N),
74
+ 'C11' => %w(C11B C11C C11D),
75
+ 'C12' => %w(C12C C12F C12G C12H C12J C12L C12M C12N C12P C12Q C12R),
76
+ 'C13' => %w(C13B C13K),
77
+ 'C14' => %w(C14B C14C),
78
+ 'C21' => %w(C21B C21C C21D),
79
+ 'C22' => %w(C22B C22C C22F),
80
+ 'C23' => %w(C23C C23D C23F C23G),
81
+ 'C25' => %w(C25B C25C C25D C25F),
82
+ 'C30' => ['C30B'],
83
+ 'C40' => ['C40B'],
84
+ 'C99' => ['C99Z'],
85
+ 'D' => %w(D01 D02 D03 D04 D05 D06 D07 D21 D99),
86
+ 'D01' => %w(D01B D01C D01D D01F D01G D01H),
87
+ 'D02' => %w(D02G D02H D02J),
88
+ 'D03' => %w(D03C D03D D03J),
89
+ 'D04' => %w(D04B D04C D04D D04G D04H),
90
+ 'D05' => %w(D05B D05C),
91
+ 'D06' => %w(D06B D06C D06F D06G D06H D06J D06L D06M D06N D06P D06Q),
92
+ 'D07' => ['D07B'],
93
+ 'D21' => %w(D21B D21C D21D D21F D21G D21H D21J),
94
+ 'D99' => ['D99Z'],
95
+ 'E' => %w(E01 E02 E03 E04 E05 E06 E21 E99),
96
+ 'E01' => %w(E01B E01C E01D E01F E01H),
97
+ 'E02' => %w(E02B E02C E02D E02F),
98
+ 'E03' => %w(E03B E03C E03D E03F),
99
+ 'E04' => %w(E04B E04C E04D E04F E04G E04H),
100
+ 'E05' => %w(E05B E05C E05D E05F E05G),
101
+ 'E06' => %w(E06B E06C),
102
+ 'E21' => %w(E21B E21C E21D E21F),
103
+ 'E99' => ['E99Z'],
104
+ 'F' => %w(F01 F02 F03 F04 F15 F16 F17 F21 F22 F23 F24 F25 F26 F27 F28 F41 F42 F99),
105
+ 'F01' => %w(F01B F01C F01D F01K F01L F01M F01N F01P),
106
+ 'F02' => %w(F02B F02C F02D F02F F02G F02K F02M F02N F02P),
107
+ 'F03' => %w(F03B F03C F03D F03G F03H),
108
+ 'F04' => %w(F04B F04C F04D F04F),
109
+ 'F15' => %w(F15B F15C F15D),
110
+ 'F16' => %w(F16B F16C F16D F16F F16G F16H F16J F16K F16L F16M F16N F16P F16S F16T),
111
+ 'F17' => %w(F17B F17C F17D),
112
+ 'F21' => %w(F21H F21K F21L F21S F21V F21W F21Y),
113
+ 'F22' => %w(F22B F22D F22G),
114
+ 'F23' => %w(F23B F23C F23D F23G F23H F23J F23K F23L F23M F23N F23Q F23R),
115
+ 'F24' => %w(F24B F24C F24D F24F F24H F24J),
116
+ 'F25' => %w(F25B F25C F25D F25J),
117
+ 'F26' => ['F26B'],
118
+ 'F27' => %w(F27B F27D),
119
+ 'F28' => %w(F28B F28C F28D F28F F28G),
120
+ 'F41' => %w(F41A F41B F41C F41F F41G F41H F41J),
121
+ 'F42' => %w(F42B F42C F42D),
122
+ 'F99' => ['F99Z'],
123
+ 'G' => %w(G01 G02 G03 G04 G05 G06 G07 G08 G09 G10 G11 G12 G21 G99),
124
+ 'G01' => %w(G01B G01C G01D G01F G01G G01H G01J G01K G01L G01M G01N G01P G01Q G01R G01S G01T G01V G01W),
125
+ 'G02' => %w(G02B G02C G02F),
126
+ 'G03' => %w(G03B G03C G03D G03F G03G G03H),
127
+ 'G04' => %w(G04B G04C G04D G04F G04G G04R),
128
+ 'G05' => %w(G05B G05D G05F G05G),
129
+ 'G06' => %w(G06C G06D G06E G06F G06G G06J G06K G06M G06N G06Q G06T),
130
+ 'G07' => %w(G07B G07C G07D G07F G07G),
131
+ 'G08' => %w(G08B G08C G08G),
132
+ 'G09' => %w(G09B G09C G09D G09F G09G),
133
+ 'G10' => %w(G10B G10C G10D G10F G10G G10H G10K G10L),
134
+ 'G11' => %w(G11B G11C),
135
+ 'G12' => ['G12B'],
136
+ 'G21' => %w(G21B G21C G21D G21F G21G G21H G21J G21K),
137
+ 'G99' => ['G99Z'],
138
+ 'H' => %w(H01 H02 H03 H04 H05 H99),
139
+ 'H01' => %w(H01B H01C H01F H01G H01H H01J H01K H01L H01M H01P H01Q H01R H01S H01T),
140
+ 'H02' => %w(H02B H02G H02H H02J H02K H02M H02N H02P H02S),
141
+ 'H03' => %w(H03B H03C H03D H03F H03G H03H H03J H03K H03L H03M),
142
+ 'H04' => %w(H04B H04H H04J H04K H04L H04M H04N H04Q H04R H04S H04W),
143
+ 'H05' => %w(H05B H05C H05F H05G H05H H05K),
144
+ 'H99' => ['H99Z'] }
145
+ end
146
+ end
@@ -0,0 +1,60 @@
1
+ require 'httparty'
2
+ require 'epo_ops/ipc_class_util'
3
+
4
+ module EpoOps
5
+ # Usually this should only used internally.
6
+ # Loads the Hierarchy from the WIPO.
7
+ # This is used to update IpcClassHierarchy manually.
8
+ # At the beginning of the year the WIPO publishes a new list of IPC classes.
9
+ # The IpcClassHierarchy should then be updated. Make sure that the url is
10
+ # correct!
11
+ class IpcClassHierarchyLoader
12
+ # loads data from the WIPO
13
+ # @return [Hash]
14
+ def self.load
15
+ load_url
16
+ end
17
+
18
+ private
19
+
20
+ def self.load_url
21
+ url = 'http://www.wipo.int/ipc/itos4ipc/ITSupport_and_download_area/20160101/IPC_scheme_title_list/EN_ipc_section_#letter_title_list_20160101.txt'
22
+
23
+ # There is a file for every letter A-H
24
+ ('A'..'H').inject({}) do |mem, letter|
25
+ # Fetch the file from the server
26
+ response = HTTParty.get(url.gsub('#letter', letter), http_proxyaddr: proxy[:addr], http_proxyport: proxy[:port])
27
+ file = response.body
28
+ mem.merge! process_file(file)
29
+ end
30
+ end
31
+
32
+ def self.process_file(file)
33
+ # Process every line (There is a line for every class entry, name and description are separated by a \t)
34
+ file.each_line.inject(Hash.new { |h, k| h[k] = [] }) do |mem, line|
35
+ next if line.to_s.strip.empty?
36
+ ipc_class_generic, description = line.split("\t")
37
+
38
+ # Some entries in the files have the same ipc class, the first line is
39
+ # just some kind of headline, the second is the description we want.
40
+ ipc_class = EpoOps::IpcClassUtil.parse_generic_format(ipc_class_generic)
41
+ if ipc_class.length == 3
42
+ mem[ipc_class[0]] << ipc_class
43
+ elsif ipc_class.length == 4
44
+ mem[ipc_class[0, 3]] << ipc_class
45
+ end
46
+ mem
47
+ end
48
+ end
49
+
50
+ def self.proxy
51
+ # configure proxy
52
+ proxy_addr = nil
53
+ proxy_port = nil
54
+ unless ENV['http_proxy'].to_s.strip.empty?
55
+ proxy_addr, proxy_port = ENV['http_proxy'].gsub('http://', '').gsub('/', '').split(':')
56
+ end
57
+ { addr: proxy_addr, port: proxy_port }
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,71 @@
1
+ require 'epo_ops/ipc_class_hierarchy'
2
+
3
+ module EpoOps
4
+ # Utility functions to work on Strings representing ipc classes.
5
+ class IpcClassUtil
6
+
7
+ # @return [Array] \['A', 'B', …, 'H'\]
8
+ def self.main_classes
9
+ %w( A B C D E F G H )
10
+ end
11
+
12
+ # check if the given ipc_class is valid as OPS search parameter
13
+ # @param [String] ipc_class an ipc class
14
+ # @return [Boolean]
15
+ def self.valid_for_search?(ipc_class)
16
+ ipc_class.match(/\A[A-H](\d{2}([A-Z](\d{1,2}\/\d{2,3})?)?)?\z/)
17
+ end
18
+
19
+ # There is a generic format for ipc classes that does not have
20
+ # the / as delimiter and leaves space for additions. This parses
21
+ # it into the format the register search understands
22
+ # @param [String] generic ipc class in generic format
23
+ # @return [String] reformatted ipc class
24
+ # @example
25
+ # parse_generic_format('A01B0003140000') #=> 'A01B3/14'
26
+ def self.parse_generic_format(generic)
27
+ ipc_class = generic
28
+ if ipc_class.length > 4
29
+ match = ipc_class.match(/([A-Z]\d{2}[A-Z])(\d{4})(\d{6})$/)
30
+ ipc_class = match[1] + (match[2].to_i).to_s + '/' + process_number(match[3])
31
+ end
32
+ ipc_class
33
+ end
34
+
35
+ # @param [String] ipc_class an ipc_class
36
+ # @return [Array] List of all ipc classes one level more specific.
37
+ # @example
38
+ # children('A') #=> ['A01', 'A21', 'A22', 'A23', ...]
39
+ # children('A62') #=> ['A62B', 'A62C', 'A62D'],
40
+ # @raise [InvalidIpcClassError] if parameter is not a valid ipc class in
41
+ # the format EPO understands
42
+ # @raise [LevelNotSupportedError] for parameters with ipc class depth >= 3
43
+ # e.g. 'A62B' cannot be split further. It is currently not necessary to
44
+ # do so, it would only blow up the gem, and you do not want to query for
45
+ # all classes at the lowest level, as it takes too many requests.
46
+ def self.children(ipc_class)
47
+ return main_classes if ipc_class.nil?
48
+ valid = valid_for_search?(ipc_class)
49
+ fail InvalidIpcClassError, ipc_class unless valid
50
+ map = IpcClassHierarchy::Hierarchy
51
+ fail LevelNotSupportedError, ipc_class unless map.key? ipc_class
52
+ map[ipc_class]
53
+ end
54
+
55
+ # An ipc class in invalid format was given, or none at all.
56
+ class InvalidIpcClassError < StandardError; end
57
+ # It is currently not supported to split by the most specific class level.
58
+ # This would result in a large amount of requests.
59
+ class LevelNotSupportedError < StandardError; end
60
+
61
+ private
62
+
63
+ def self.process_number(number)
64
+ result = number.gsub(/0+$/, '')
65
+ result += '0' if result.length == 1
66
+ result = '00' if result.length == 0
67
+
68
+ result
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,20 @@
1
+ module EpoOps
2
+ # The register search is limited by some parameters. With one
3
+ # query one may only request as many as
4
+ # {EpoOps::Limits::MAX_QUERY_INTERVAL} references at once.
5
+ # Considering this, you have to split your requests by this
6
+ # interval. Nevertheless, the maximum value you may use is
7
+ # {EpoOps::Limits::MAX_QUERY_RANGE}. If you want to retrieve more
8
+ # references you must split by other parameters.
9
+ # @see Register
10
+ class Limits
11
+ # @return [Integer] The range in which you can search is limited, say you
12
+ # cannot request all patents of a given class at once, you probably must
13
+ # split your requests by additional conditions.
14
+ MAX_QUERY_RANGE = 2000
15
+
16
+ # @return [Integer] The maximum number of elements you may search with one
17
+ # query. Ignoring this will result in errors.
18
+ MAX_QUERY_INTERVAL = 100
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ module EpoOps
2
+ # Simple logger writing some notifications to standard output.
3
+ class Logger
4
+ # Just hands the parameter to puts.
5
+ def self.log(output)
6
+ puts output
7
+ end
8
+
9
+ # Debug logging only
10
+ def self.debug(output)
11
+ log(output) if ENV['DEBUG']
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ module EpoOps
2
+ # Used to represent persons or companies (or both) in patents. Used for
3
+ # both, agents and applicants. Most of the time, when `name` is a person
4
+ # name, `address1` is a company name. Be aware that the addresses are in
5
+ # their respective local format.
6
+ #
7
+ # Current patents usually at least use the fields address1-3, so they should
8
+ # nearly always have values. Nevertheless, older ones often only use 1-2.
9
+ # Note also that EPOs schema documents fields like `street` or `city`, but
10
+ # by now they have not been used yet.
11
+ #
12
+ # @attr [String] name the name of an entity (one or more persons or
13
+ # companies)
14
+ # @attr [String] address1 first address line. May also be a company name
15
+ # @attr [String] address2 second address line
16
+ # @attr [String] address3 third address line, may be empty
17
+ # @attr [String] address4 fourth address line, may be empty
18
+ # @attr [String] address5 fifth address line, may be empty
19
+ # @attr [String] country_code two letter country code of the address
20
+ # @attr [String] cdsid some kind of id the EPO provides, not sure yet if
21
+ # @attr [Date] occurred_on the date an address occurred on, usually matching
22
+ # the entries change_gazette_num
23
+ # usable as reference.
24
+ class NameAndAddress
25
+ attr_reader :name, :address1,
26
+ :address2,
27
+ :address3,
28
+ :address4,
29
+ :address5,
30
+ :country_code,
31
+ :cdsid,
32
+ :occurred_on
33
+
34
+ def initialize(name, address1, address2, address3, address4,
35
+ address5, country_code, cdsid, occurred_on= nil)
36
+ @address1 = address1
37
+ @address2 = address2
38
+ @address3 = address3 || ''
39
+ @address4 = address4 || ''
40
+ @address5 = address5 || ''
41
+ @name = name
42
+ @country_code = country_code || ''
43
+ @occurred_on = occurred_on || ''
44
+ @cdsid = cdsid || ''
45
+ end
46
+
47
+ # Compare addresses by the name and address fields.
48
+ # @return [Boolean]
49
+ def equal_name_and_address?(other)
50
+ name == other.name &&
51
+ address1 == other.address1 &&
52
+ address2 == other.address2 &&
53
+ address3 == other.address3 &&
54
+ address4 == other.address4 &&
55
+ address5 == other.address5
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,159 @@
1
+ module EpoOps
2
+ # This class represents a Patent Application as returned by EPO OPS returns for bibliographic
3
+ # documents.
4
+ # Some elements are not yet fully parsed but hashes returned instead.
5
+ # Not all information available is parsed, but the full data can be accesses via {#raw_data}
6
+ class PatentApplication
7
+
8
+ class << self
9
+
10
+ # Finds an application document by application number. As an application may have several numbers assigned
11
+ # (for example EP and WO) we pick the first one returned - thus the returned document may have a different number
12
+ # @param application_number [String] identifies the application document at EPO
13
+ # @return [PatentApplication] the application document, nil if it can't be found
14
+ # @note API url: /3.1/rest-services/register/application/epodoc/#{application_number}/biblio
15
+ def find(application_number)
16
+ raw_data = EpoOps::Client.request(
17
+ :get,
18
+ "/3.1/rest-services/register/application/epodoc/#{application_number}/biblio"
19
+ ).parsed
20
+
21
+ data = EpoOps::Util.flat_dig(
22
+ raw_data,
23
+ 'world_patent_data', 'register_search', 'register_documents', 'register_document'
24
+ ).first
25
+
26
+ return nil unless data
27
+
28
+ Factories::PatentApplicationFactory.build(data)
29
+ end
30
+
31
+ # Searches for application documents using a CQl query
32
+ # @see EpoOps::SearchQueryBuilder
33
+ # @see EpoOps::RegisterSearchResult
34
+ # Returned documents are not fully populated with data,
35
+ # only publication references, application id and IPC classes are available
36
+ # to retrive all data use {#fetch}
37
+ #
38
+ # @param cql_query [String] a CQL query string
39
+ # @return [RegisterSearchResult]
40
+ # @note API url: /3.1/rest-services/register/search
41
+ def search(cql_query)
42
+ data = Client.request(
43
+ :get,
44
+ '/3.1/rest-services/register/search?' + cql_query
45
+ ).parsed
46
+
47
+ EpoOps::Factories::RegisterSearchResultFactory.build(data)
48
+ rescue EpoOps::Error::NotFound
49
+ EpoOps::RegisterSearchResult::NullResult.new
50
+ end
51
+ end
52
+
53
+ # A number by which a patent is uniquely identifiable and querieable.
54
+ # The first two letters are the country code of the processing patent
55
+ # office, for european patents this is EP.
56
+ # @return [String] application number.
57
+ attr_reader :application_nr
58
+
59
+ # @return [Hash] The raw application data as recived from EPO
60
+ attr_reader :raw_data
61
+
62
+ # @return [Array] a list of the IPC-Classifications, as strings.
63
+ # Format is set by EPO, should be similar to: E06B7/23
64
+ attr_reader :classifications
65
+
66
+ # Lists the Applicants of the Application
67
+ # Applicants are subject to change at EPO, often
68
+ # their names or addresses are updated, sometimes other
69
+ # people/companies appear or disappear.
70
+ # @return [Array] Array of {EpoOps::NameAndAddress}
71
+ attr_reader :applicants
72
+
73
+ # Lists the Agents of the Application
74
+ # Agents are subject to change at EPO, often
75
+ # their names or addresses are updated, sometimes other
76
+ # people/companies appear or disappear.
77
+ # @return [Array] Array of {EpoOps::NameAndAddress}
78
+ attr_reader :agents
79
+
80
+ # Lists the Inventors of the Application
81
+ # Agents are subject to change at EPO, often
82
+ # their names or addresses are updated, sometimes other
83
+ # people/companies appear or disappear.
84
+ # @return [Array] Array of {EpoOps::NameAndAddress}
85
+ attr_reader :inventors
86
+
87
+ # @return [String] the string representation of the current patent status as
88
+ # described by the EPO
89
+ attr_reader :status
90
+
91
+ # The priority claim describe the first documents that were filed at any
92
+ # patent office in the world regarding this patent.
93
+ # @return [Array] an Array of hashes which descibe the filed priorities with the fields:
94
+ # `country` `doc_number`, `date`, `kind`, and `sequence`
95
+ attr_reader :priority_claims
96
+
97
+ # @return [Array] List of hashes containing information about publications
98
+ # made, entries exist for multiple types of publications, e.g. A1, B1.
99
+ attr_reader :publication_references
100
+
101
+ attr_reader :effective_date
102
+
103
+ def initialize(application_nr, data={})
104
+ @application_nr = application_nr
105
+ data.each_pair do |key,value|
106
+ instance_variable_set("@#{key.to_s}",value)
107
+ end
108
+ end
109
+
110
+ # Returns the Application title in the given languages
111
+ # @param lang [Integer] language identifier for the title
112
+ # @return [String] the english title of the patent
113
+ # @note Titles are usually available at least in english, french and german.
114
+ # Other languages are also possible.
115
+ def title(lang='en')
116
+ return nil unless @title.instance_of?(Hash)
117
+ @title[lang]
118
+ end
119
+
120
+ # Many fields of the XML the EPO provides have a field
121
+ # `change_gazette_num`. It is a commercial date (year + week)
122
+ # that describes in which week the element has been
123
+ # changed. This method parses them and returns the most recent
124
+ # date found.
125
+ # @return [Date] the latest date found in the document.
126
+ def latest_update
127
+ gazette_nums = EpoOps::Util.parse_hash_flat(@raw_data, 'change_gazette_num')
128
+ nums = gazette_nums.map { |num| EpoOps::Util.parse_change_gazette_num(num) }.keep_if { |match| !match.nil? }
129
+ nums.max
130
+ end
131
+
132
+ # @return [String] The URL at which you can query the original document.
133
+ def url
134
+ @url ||= "https://ops.epo.org/3.1/rest-services/register/application/epodoc/#{application_nr}"
135
+ end
136
+
137
+ # Fetches the same document from the register populating all available fields
138
+ # @see {PatentApplication.find}
139
+ # @return [self]
140
+ def fetch
141
+ raise "Application Number must be set!" unless application_nr
142
+
143
+ new_data = self.class.find(application_nr)
144
+
145
+ @raw_data = new_data.raw_data
146
+ @title = new_data.instance_variable_get('@title')
147
+ @status = new_data.status
148
+ @agents = new_data.agents
149
+ @applicants = new_data.applicants
150
+ @inventors = new_data.inventors
151
+ @classifications = new_data.classifications
152
+ @priority_claims = new_data.priority_claims
153
+ @publication_references = new_data.publication_references
154
+ @effective_date = new_data.effective_date
155
+
156
+ self
157
+ end
158
+ end
159
+ end