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 +5 -5
- data/.github/workflows/ruby.yml +35 -0
- data/.gitignore +2 -0
- data/Gemfile +1 -1
- data/README.md +69 -47
- data/Rakefile +9 -1
- data/lib/sec_query.rb +1 -3
- data/lib/sec_query/entity.rb +3 -7
- data/lib/sec_query/filing.rb +19 -18
- data/lib/sec_query/filing_detail.rb +68 -0
- data/lib/sec_query/sec_uri.rb +5 -5
- data/lib/sec_query/version.rb +1 -1
- data/sec_query.gemspec +13 -10
- data/spec/sec_query/entity_spec.rb +35 -2
- data/spec/sec_query/filing_spec.rb +96 -81
- data/spec/sec_query/sec_uri_spec.rb +7 -7
- data/spec/spec_helper.rb +8 -8
- metadata +81 -34
- data/lib/sec_query/relationship.rb +0 -72
- data/lib/sec_query/transaction.rb +0 -106
- data/spec/sec_query/transaction_spec.rb +0 -39
- data/spec/support/filings/filing.txt +0 -257
- data/spec/support/idx/test.idx +0 -4639
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e05c826068c7e3b70b62043aa111e83bd01118130116a9db4d6e7991ea923231
|
4
|
+
data.tar.gz: 38465104422bc0096f3c4a432f0b6801da6282e87c8f7e5e0e6468b7606f5d30
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/Gemfile
CHANGED
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
|
-
###
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
87
|
+
##### .filings
|
74
88
|
|
75
|
-
|
89
|
+
Returns a list of Sec::Filing instances for an Sec::Entity
|
76
90
|
|
77
|
-
|
91
|
+
### SecQuery::Filing
|
78
92
|
|
79
|
-
|
93
|
+
SecQuery::Filing instance may contains the following attributes:
|
80
94
|
|
81
|
-
|
95
|
+
* cik
|
96
|
+
* title
|
97
|
+
* symmary
|
98
|
+
* link
|
99
|
+
* term
|
100
|
+
* date
|
101
|
+
* file_id
|
102
|
+
* detail
|
82
103
|
|
83
|
-
|
104
|
+
#### Class Methods
|
84
105
|
|
85
|
-
|
106
|
+
##### .recent
|
86
107
|
|
87
|
-
|
108
|
+
Find recent filings:
|
88
109
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
117
|
+
Requires a block. Returns the most recent filings. Use start, count and limit to iterate through recent filings.
|
102
118
|
|
103
|
-
|
104
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
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/
|
15
|
+
require 'sec_query/filing_detail'
|
17
16
|
require 'sec_query/sec_uri'
|
18
|
-
require 'sec_query/transaction'
|
19
17
|
require 'sec_query/version'
|
data/lib/sec_query/entity.rb
CHANGED
@@ -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
|
data/lib/sec_query/filing.rb
CHANGED
@@ -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
|
-
|
19
|
-
parse_rss(
|
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
|
-
|
50
|
-
|
51
|
-
|
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] = "
|
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-
|
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
|
data/lib/sec_query/sec_uri.rb
CHANGED
@@ -25,9 +25,9 @@ module SecQuery
|
|
25
25
|
|
26
26
|
def self.for_date(date)
|
27
27
|
instance = SecURI.new
|
28
|
-
instance.
|
29
|
-
instance.
|
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 <=
|
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 = '
|
84
|
+
self.scheme = 'https'
|
85
85
|
self.path = 'cgi-bin'
|
86
86
|
end
|
87
87
|
|