sec_query 1.1.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 10cc642633f4e01fb1bebd0d1f611766e3f93a69
4
- data.tar.gz: 2f50d5d488d8504299e2fd01f852e76b784aec2a
2
+ SHA256:
3
+ metadata.gz: e05c826068c7e3b70b62043aa111e83bd01118130116a9db4d6e7991ea923231
4
+ data.tar.gz: 38465104422bc0096f3c4a432f0b6801da6282e87c8f7e5e0e6468b7606f5d30
5
5
  SHA512:
6
- metadata.gz: 4f5846cda290b9c3f2182460842e9263c1a2d60bbb5ae07d2abce512ce15d6f47220acaac721777af46b38b487115708db9652d4e911b618a965bb1baaea9fa6
7
- data.tar.gz: 0053b07879bf113fb79127bcac37decc9b6a8569f12404597e81827940fe9923110515e31f074f2ef61056560e350c6e4195016746ebe4d6e34e5bbb47c634af
6
+ metadata.gz: 538a894e0910434e9ed11c604485342919d6d9de42da73b3d86b2da57e1cd7b7b4b04bdeb51f405e5c1d5b7ff0e01eed6ae8e9eaa919c73ccc7af98af102a44f
7
+ data.tar.gz: 46ea472a4334afca91affa041ba5ee2bbc5879b9009c7ab2d27e05e2f884c056e77146e9ca88dbb9785c8774f4662b6c178e6995986f2a22a898307d754ef8ef
@@ -0,0 +1,35 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ main ]
13
+ pull_request:
14
+ branches: [ main ]
15
+
16
+ jobs:
17
+ test:
18
+
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ matrix:
22
+ ruby-version: ['2.6', '2.7', '3.0']
23
+
24
+ steps:
25
+ - uses: actions/checkout@v2
26
+ - name: Set up Ruby
27
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
29
+ # uses: ruby/setup-ruby@v1
30
+ uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
31
+ with:
32
+ ruby-version: ${{ matrix.ruby-version }}
33
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
34
+ - name: Run tests
35
+ run: bundle exec rake
data/.gitignore CHANGED
@@ -4,6 +4,8 @@
4
4
  .bundle
5
5
  .config
6
6
  .yardoc
7
+ .ruby-gemset
8
+ .ruby-version
7
9
  Gemfile.lock
8
10
  InstalledFiles
9
11
  _yardoc
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in sec_query.gemspec
4
4
  gemspec
data/README.md CHANGED
@@ -6,6 +6,10 @@ Look-up an Entity - person or company - by Central Index Key (CIK), stock symbol
6
6
 
7
7
  Additionally retrieve some, or all, Relationships, Transactions and Filings as recorded by the SEC.
8
8
 
9
+ ## Note: 9/13/16, SEC.GOV embraces SSL!
10
+
11
+ On or before Septmember 13th, 2016, the SEC.gov updated their site to use SSL (Huzzah!). Version 1.2.0 addresses this change. All versions less than 1.2.0, will cease to function as expected. Update immediately.
12
+
9
13
  ## Installation
10
14
 
11
15
  To install the 'sec_query' Ruby Gem run the following command at the terminal prompt.
@@ -24,9 +28,29 @@ If running 'sec_query' from the command prompt in irb:
24
28
 
25
29
  ## Functionality
26
30
 
27
- ### FIND COMPANY:
31
+ ### Entity:
32
+
33
+ An Sec::Entity instance contains the following attributes:
34
+
35
+ * cik
36
+ * name
37
+ * mailing_address
38
+ * business_adddress
39
+ * assigned_sic
40
+ * assigned_sic_desc
41
+ * assigned_sic_href
42
+ * assitant_director
43
+ * cik_href
44
+ * formerly_name
45
+ * state_location
46
+ * state_location_href
47
+ * state_of_incorporation
48
+
49
+ #### Class Methods
50
+
51
+ ##### .find
28
52
 
29
- #### By Stock Symbol:
53
+ ###### By Stock Symbol:
30
54
 
31
55
  `SecQuery::Entity.find("aapl")`
32
56
 
@@ -34,7 +58,7 @@ Or:
34
58
 
35
59
  `SecQuery::Entity.find({:symbol=> "aapl"})`
36
60
 
37
- #### By Name:
61
+ ###### By Name:
38
62
 
39
63
  `SecQuery::Entity.find("Apple, Inc.")`
40
64
 
@@ -42,7 +66,7 @@ Or:
42
66
 
43
67
  `SecQuery::Entity.find({:name=> "Apple, Inc."})`
44
68
 
45
- #### By Central Index Key, CIK:
69
+ ###### Central Index Key, CIK
46
70
 
47
71
  `SecQuery::Entity.find( "0000320193")`
48
72
 
@@ -50,7 +74,7 @@ Or:
50
74
 
51
75
  `SecQuery::Entity.find({:cik=> "0000320193"})`
52
76
 
53
- #### FIND PERSON:
77
+ ###### By First, Middle and Last Name:
54
78
 
55
79
  By First, Middle and Last Name.
56
80
 
@@ -58,61 +82,59 @@ By First, Middle and Last Name.
58
82
 
59
83
  Middle initial or name is optional, but helps when there are multiple results for First and Last Name.
60
84
 
61
- ### RELATIONSHIPS, TRANSACTIONS, FILINGS
62
-
63
- To return everything - All Relationships, Transactions and Filings - that the SEC Edgar system has stored on a company or person, do any of the following commands (They all do the same thing.):
64
-
65
- `SecQuery::Entity.find("AAPL", true)`
66
-
67
- `SecQuery::Entity.find("AAPL", true, true, true)`
68
-
69
- `SecQuery::Entity.find("AAPL", {:relationships=> true, :transactions=> true, :filings=>true})`
70
-
71
- `SecQuery::Entity.find("AAPL", :relationships=> true, :transactions=> true, :filings=>true)`
85
+ #### Instance Methods
72
86
 
73
- `SecQuery::Entity.find("AAPL", :relationships=> true, :transactions=> {:start=> 0, :count=> 80}, :filings=>{:start=> 0, :count=> 80})`
87
+ ##### .filings
74
88
 
75
- You may also limit either the transactions or filings by adding the :limit to the transaction or filing arguements.
89
+ Returns a list of Sec::Filing instances for an Sec::Entity
76
90
 
77
- For example,
91
+ ### SecQuery::Filing
78
92
 
79
- `SecQuery::Entity.find("AAPL", :relationships=> true, :transactions=> {:start=> 0, :count=>20, :limit=> 20}, :filings=>{:start=> 0, :count=> 20, :limit=> 20})`
93
+ SecQuery::Filing instance may contains the following attributes:
80
94
 
81
- The above query will only return the last 20 transactions and filings. This is helpful when querying companies that may have thousands or tens of thousands of transactions or filings.
95
+ * cik
96
+ * title
97
+ * symmary
98
+ * link
99
+ * term
100
+ * date
101
+ * file_id
102
+ * detail
82
103
 
83
- ## Classes
104
+ #### Class Methods
84
105
 
85
- This gem contains four classes - Entity, Relationship, Transaction and Filing. Each Class contains the listed fields. (Everything I could parse out of the query results.)
106
+ ##### .recent
86
107
 
87
- * Entity
108
+ Find recent filings:
88
109
 
89
- `:first, :middle, :last, :name, :symbol, :cik, :url, :type, :sic, :location, :state_of_inc, :formerly, :mailing_address, :business_address, :relationships, :transactions, :filings`
90
-
91
- * Relationship
92
-
93
- `:name, :position, :date, :cik`
94
-
95
- * Transaction
96
-
97
- `:filing_number, :code, :date, :reporting_owner, :form, :type, :modes, :shares, :price, :owned, :number, :owner_cik, :security_name, :deemed, :exercise, :nature, :derivative, :underlying_1, :exercised, :underlying_2, :expires, :underlying_3`
98
-
99
- * Filing
110
+ ```
111
+ filings = []
112
+ SecQuery::Filing.recent(start: 0, count: 10, limit: 10) do |filing|
113
+ filings.push filing
114
+ end
115
+ ```
100
116
 
101
- `:cik, :title, :summary, :link, :term, :date, :file_id`
117
+ Requires a block. Returns the most recent filings. Use start, count and limit to iterate through recent filings.
102
118
 
103
- Filings can are fetched a few different ways. Here are some of the supported
104
- methods:
119
+ ### SecQuery::FilingDetail
120
+ Represents the detail page for a given filing.
121
+ Ex: [Filing Detail page](https://www.sec.gov/Archives/edgar/data/320193/000032019317000070/0000320193-17-000070-index.htm) of Apple's Annual Report from 2017
105
122
 
106
- ```rb
107
- # prints the links for the most recent filings
108
- SecQuery::Filing.recent do |filing|
109
- p filing.link
110
- end
123
+ #### Instance Methods
124
+ * link
125
+ * filing_date
126
+ * accepted_date
127
+ * period_of_report
128
+ * sec_access_number
129
+ * document_count
130
+ * format_files
131
+ * data_files
111
132
 
112
- # prints all of the links for cik 0000704051 (LEGG MASON, INC.)
113
- SecQuery::Filing.for_cik('0000704051') do |filing|
114
- p filing.link
115
- end
133
+ #### Class Methods
134
+ ##### .fetch
135
+ ```
136
+ appl_10k_details_url = 'https://www.sec.gov/Archives/edgar/data/320193/000032019317000070/0000320193-17-000070-index.htm'
137
+ filing_detail = SecQuery::FilingDetail.fetch(appl_10k_details_url)
116
138
  ```
117
139
 
118
140
  ## To Whom It May Concern at the SEC
data/Rakefile CHANGED
@@ -1 +1,9 @@
1
- require 'bundler/gem_tasks'
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task test: :spec
7
+
8
+ task default: :test
9
+
data/lib/sec_query.rb CHANGED
@@ -4,7 +4,6 @@
4
4
  require 'active_support/all'
5
5
  require 'addressable/uri'
6
6
  require 'open-uri'
7
- require 'net/ftp'
8
7
  require 'rest-client'
9
8
  require 'rss'
10
9
  require 'nokogiri'
@@ -13,7 +12,6 @@ require 'rubygems'
13
12
  # internal
14
13
  require 'sec_query/entity'
15
14
  require 'sec_query/filing'
16
- require 'sec_query/relationship'
15
+ require 'sec_query/filing_detail'
17
16
  require 'sec_query/sec_uri'
18
- require 'sec_query/transaction'
19
17
  require 'sec_query/version'
@@ -15,12 +15,8 @@ module SecQuery
15
15
  end
16
16
  end
17
17
 
18
- def filings
19
- Filing.find(@cik)
20
- end
21
-
22
- def transactions
23
- Transaction.find(@cik)
18
+ def filings(args={})
19
+ Filing.find(@cik, 0, 80, args)
24
20
  end
25
21
 
26
22
  def self.query(url)
@@ -53,7 +49,7 @@ module SecQuery
53
49
  content['formerly_names'] = content.delete('formerly_names')['names']
54
50
  end
55
51
  content['addresses']['address'].each do |address|
56
- content["#{address['type']}_address"] = address
52
+ content["#{address['type']}_address"] = address unless address.nil?
57
53
  end
58
54
  return content
59
55
  else
@@ -14,9 +14,13 @@ module SecQuery
14
14
  end
15
15
  end
16
16
 
17
+ def detail
18
+ @detail ||= FilingDetail.fetch(@link)
19
+ end
20
+
17
21
  def self.fetch(uri, &blk)
18
- open(uri) do |rss|
19
- parse_rss(rss, &blk)
22
+ RestClient::Request.execute(method: :get, url: uri.to_s, timeout: 10) do |response, request, result, &block|
23
+ parse_rss(response.body, &blk)
20
24
  end
21
25
  end
22
26
 
@@ -46,20 +50,10 @@ module SecQuery
46
50
  end
47
51
 
48
52
  def self.for_date(date, &blk)
49
- ftp = Net::FTP.new('ftp.sec.gov')
50
- ftp.login
51
- file_name = ftp.nlst("edgar/daily-index/#{ date.to_sec_uri_format }*")[0]
52
- ftp.close
53
- open("ftp://ftp.sec.gov/#{ file_name }") do |file|
54
- if file_name[-2..-1] == 'gz'
55
- gz_reader = Zlib::GzipReader.new(file)
56
- gz_reader.rewind
57
- filings_for_index(gz_reader).each(&blk)
58
- else
59
- filings_for_index(file).each(&blk)
60
- end
53
+ url = SecURI.for_date(date).to_s
54
+ RestClient::Request.execute(method: :get, url: url, timeout: 10) do |response, request, result, &block|
55
+ filings_for_index(response.body).each(&blk)
61
56
  end
62
- rescue Net::FTPTempError
63
57
  end
64
58
 
65
59
  def self.filings_for_index(index)
@@ -82,7 +76,7 @@ module SecQuery
82
76
  return nil unless Regexp.new(/\d{8}/).match(data[3])
83
77
  return nil if data[4] == nil
84
78
  unless data[4][0..3] == 'http'
85
- data[4] = "http://www.sec.gov/Archives/#{ data[4] }"
79
+ data[4] = "https://www.sec.gov/Archives/#{ data[4] }"
86
80
  end
87
81
  begin
88
82
  Date.parse(data[3])
@@ -134,22 +128,29 @@ module SecQuery
134
128
  end
135
129
  end
136
130
 
137
- def self.find(cik, start = 0, count = 80)
131
+ def self.find(cik, start = 0, count = 80, args={})
138
132
  temp = {}
139
133
  temp[:url] = SecURI.browse_edgar_uri({cik: cik})
140
134
  temp[:url][:action] = :getcompany
141
135
  temp[:url][:start] = start
142
136
  temp[:url][:count] = count
137
+ args.each {|k,v| temp[:url][k]=v }
138
+
143
139
  response = Entity.query(temp[:url].output_atom.to_s)
144
140
  document = Nokogiri::HTML(response)
145
141
  parse(cik, document)
146
142
  end
147
143
 
144
+ def self.last(cik, args = {})
145
+ filings = find(cik, 0, 1, args)
146
+ filings.is_a?(Array) ? filings.first : nil
147
+ end
148
+
148
149
  def self.parse(cik, document)
149
150
  filings = []
150
151
  if document.xpath('//content').to_s.length > 0
151
152
  document.xpath('//content').each do |e|
152
- if e.xpath('//content/accession-nunber').to_s.length > 0
153
+ if e.xpath('//content/accession-number').to_s.length > 0
153
154
  content = Hash.from_xml(e.to_s)['content']
154
155
  content[:cik] = cik
155
156
  content[:file_id] = content.delete('accession_nunber')
@@ -0,0 +1,68 @@
1
+ # encoding: UTF-8
2
+
3
+ module SecQuery
4
+ # => SecQuery::FilingDetail
5
+ # SecQuery::FilingDetail requests and parses Filing Detail for any given SecQuery::Filing
6
+ class FilingDetail
7
+ COLUMNS = [:link, :filing_date, :accepted_date, :period_of_report, :sec_access_number, :document_count, :format_files, :data_files]
8
+
9
+ attr_accessor(*COLUMNS)
10
+
11
+ def initialize(filing_detail)
12
+ COLUMNS.each do |column|
13
+ instance_variable_set("@#{ column }", filing_detail[column])
14
+ end
15
+ end
16
+
17
+ def self.fetch(uri)
18
+ response = RestClient::Request.execute(method: :get, url: uri.to_s.gsub('http:', 'https:'), timeout: 10)
19
+ document = Nokogiri::HTML(response.body)
20
+ filing_date = document.xpath('//*[@id="formDiv"]/div[2]/div[1]/div[2]').text
21
+ accepted_date = document.xpath('//*[@id="formDiv"]/div[2]/div[1]/div[4]').text
22
+ period_of_report = document.xpath('//*[@id="formDiv"]/div[2]/div[2]/div[2]').text
23
+ sec_access_number = document.xpath('//*[@id="secNum"]/text()').text.strip
24
+ document_count = document.xpath('//*[@id="formDiv"]/div[2]/div[1]/div[6]').text.to_i
25
+ format_files_table = document.xpath("//table[@summary='Document Format Files']")
26
+ data_files_table = document.xpath("//table[@summary='Data Files']")
27
+
28
+ format_files = (parsed = parse_files(format_files_table)) && (parsed || [])
29
+ data_files = (parsed = parse_files(data_files_table)) && (parsed || [])
30
+
31
+ new({uri: uri,
32
+ filing_date: filing_date,
33
+ accepted_date: accepted_date,
34
+ period_of_report: period_of_report,
35
+ sec_access_number: sec_access_number,
36
+ document_count: document_count,
37
+ format_files: format_files,
38
+ data_files: data_files})
39
+ end
40
+
41
+ def self.parse_files(format_files_table)
42
+ # get table headers
43
+ headers = []
44
+ format_files_table.xpath('//th').each do |th|
45
+ headers << th.text
46
+ end
47
+
48
+ # get table rows
49
+ rows = []
50
+ format_files_table.xpath('//tr').each_with_index do |row, i|
51
+ rows[i] = {}
52
+ row.xpath('td').each_with_index do |td, j|
53
+ if td.children.first && td.children.first.name == 'a'
54
+ relative_url = td.children.first.attributes.first[1].value
55
+ rows[i][headers[j]] = {
56
+ 'link' => "https://www.sec.gov#{relative_url}",
57
+ 'text' => td.text.gsub(/\A\p{Space}*/, '')
58
+ }
59
+ else
60
+ rows[i][headers[j]] = td.text.gsub(/\A\p{Space}*/, '')
61
+ end
62
+ end
63
+ end
64
+
65
+ rows.reject(&:empty?)
66
+ end
67
+ end
68
+ end
@@ -25,9 +25,9 @@ module SecQuery
25
25
 
26
26
  def self.for_date(date)
27
27
  instance = SecURI.new
28
- instance.scheme = 'ftp'
29
- instance.host = 'ftp.sec.gov'
30
- instance.path = "edgar/daily-index/#{ date.to_sec_uri_format }"
28
+ instance.host = 'www.sec.gov'
29
+ instance.scheme = 'https'
30
+ instance.path = "Archives/edgar/daily-index/#{ date.to_sec_uri_format }"
31
31
  instance
32
32
  end
33
33
 
@@ -49,7 +49,7 @@ module SecQuery
49
49
  begin Float(string_arg)
50
50
  options[:CIK] = string_arg
51
51
  rescue
52
- if string_arg.length <= 4
52
+ if string_arg.length <= 5
53
53
  options[:CIK] = string_arg
54
54
  else
55
55
  options[:company] = string_arg.gsub(/[(,?!\''"":.)]/, '')
@@ -81,7 +81,7 @@ module SecQuery
81
81
 
82
82
  def initialize
83
83
  self.host = 'www.sec.gov'
84
- self.scheme = 'http'
84
+ self.scheme = 'https'
85
85
  self.path = 'cgi-bin'
86
86
  end
87
87