uls 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e4c602c4bd5b7ceb3ee7b0378a7a0d66ec59e31e6bb82f29d8f334b7ea86c269
4
+ data.tar.gz: e7529e3a187621009257ef3b5a623103f8183233bf53d76c62115a538ae9155f
5
+ SHA512:
6
+ metadata.gz: 329af07c3c6b4aa5636a4d68b9f27b35e2626547be4e666df10b2e2c04cd04791ba0af137d51371209ec75d4bdb16471b51d77a7f9aa859958bdccc195023e20
7
+ data.tar.gz: 35bcd041e58ad4a0caa83978298152858b558814b8c57dcb46591e9a738cf05189e19edd78995953f9ca6396022e244abc6e6da4666b499b6fb598eb1c5ebe85
data/.gitignore ADDED
@@ -0,0 +1,56 @@
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
+ /tmp/
12
+ .rspec_status
13
+ .idea
14
+
15
+ # Used by dotenv library to load environment variables.
16
+ # .env
17
+
18
+ ## Specific to RubyMotion:
19
+ .dat*
20
+ .repl_history
21
+ build/
22
+ *.bridgesupport
23
+ build-iPhoneOS/
24
+ build-iPhoneSimulator/
25
+
26
+ ## Specific to RubyMotion (use of CocoaPods):
27
+ #
28
+ # We recommend against adding the Pods directory to your .gitignore. However
29
+ # you should judge for yourself, the pros and cons are mentioned at:
30
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
31
+ #
32
+ # vendor/Pods/
33
+
34
+ ## Documentation cache and generated files:
35
+ /.yardoc/
36
+ /_yardoc/
37
+ /doc/
38
+ /rdoc/
39
+
40
+ ## Environment normalization:
41
+ /.bundle/
42
+ /vendor/bundle
43
+ /lib/bundler/man/
44
+
45
+ # for a library or gem, you might want to ignore these files since the code is
46
+ # intended to run in multiple environments; otherwise, check them in:
47
+ # Gemfile.lock
48
+ # .ruby-version
49
+ # .ruby-gemset
50
+
51
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
52
+ .rvmrc
53
+
54
+ # Editor files
55
+ *.swp
56
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.1
5
+ before_install: gem install bundler -v 1.16.2
6
+ after_success: bash <(curl -s https://codecov.io/bash)
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at garth@codejunkie.org. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in uls.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,55 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ uls (0.2.0)
5
+ rubyzip (~> 2.3.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ codecov (0.6.0)
11
+ simplecov (>= 0.15, < 0.22)
12
+ coderay (1.1.2)
13
+ diff-lcs (1.3)
14
+ docile (1.4.0)
15
+ method_source (0.9.2)
16
+ pry (0.12.2)
17
+ coderay (~> 1.1.0)
18
+ method_source (~> 0.9.0)
19
+ rake (12.3.3)
20
+ rspec (3.8.0)
21
+ rspec-core (~> 3.8.0)
22
+ rspec-expectations (~> 3.8.0)
23
+ rspec-mocks (~> 3.8.0)
24
+ rspec-core (3.8.0)
25
+ rspec-support (~> 3.8.0)
26
+ rspec-expectations (3.8.3)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.8.0)
29
+ rspec-mocks (3.8.0)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.8.0)
32
+ rspec-support (3.8.0)
33
+ rubyzip (2.3.0)
34
+ simplecov (0.21.2)
35
+ docile (~> 1.1)
36
+ simplecov-html (~> 0.11)
37
+ simplecov_json_formatter (~> 0.1)
38
+ simplecov-html (0.12.3)
39
+ simplecov_json_formatter (0.1.4)
40
+ timecop (0.9.5)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ bundler (>= 2.2.33)
47
+ codecov (~> 0.6.0)
48
+ pry (~> 0.12.2)
49
+ rake (~> 12.3.3)
50
+ rspec (~> 3.0)
51
+ timecop (~> 0.9.5)
52
+ uls!
53
+
54
+ BUNDLED WITH
55
+ 2.3.16
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Garth Dubin
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ [![Build Status](https://travis-ci.com/gdubin/uls.svg?branch=main)](https://travis-ci.com/gdubin/uls) [![codecov](https://codecov.io/gh/gdubin/uls/branch/main/graph/badge.svg)](https://codecov.io/gh/gdubin/uls)
2
+
3
+ # ULS
4
+
5
+ This gem is used for parsing the database/files from the FCC Universal Licensing System. These files are often relevant to amateur radio enthusiasts.
6
+
7
+ You may download the source files from the FCC by visiting the [Universal Licensing System Databases](http://wireless.fcc.gov/uls/index.htm?job=transaction&page=weekly) page.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'uls'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle install
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install uls
24
+
25
+ ## Usage
26
+
27
+ ##### Configuration
28
+
29
+ Configuration of various options are available via the base ULS module. To configure ULS related settings:
30
+
31
+ ```
32
+ ULS.configure do |config|
33
+ config.tmpdir = '/your/tmpdir/here'
34
+ end
35
+ ```
36
+
37
+ The options currently available are as follows:
38
+
39
+ |option|default|description|
40
+ |------|-------|-----------|
41
+ |tmpdir|Dir.tmpdir|A location for ULS database files to be downloaded to and unpacked. Defaults to whatever the [Dir#tmpdir](https://ruby-doc.org/stdlib-2.5.3/libdoc/tmpdir/rdoc/Dir.html#method-c-tmpdir) is set to for your system.|
42
+
43
+ ##### Retrieving Databases
44
+
45
+ The FCC splits their data into several categories. Each category is represented as a class method on `ULS::Retriever`. From there, you may select either the daily or weekly file for a specific types. Types are driven by the category but for most categories the types are `applications` or `licenses`.
46
+
47
+ For example, to download the entire license database for amateur radio:
48
+
49
+ ```
50
+ database = ULS::Retriever.amateur_radio.weekly(:licenses)
51
+ ```
52
+
53
+ The weekly database contains all of the data and is republished weekly. Daily database updates are also provided that are much smaller and only contain the specified day's data:
54
+
55
+ ```
56
+ database = ULS::Retriever.amateur_radio.daily(:licenses)
57
+ ```
58
+
59
+ The above line will download the previous day's updates. It also takes a second parameter which corresponds to the [Date#wday](https://apidock.com/ruby/Date/wday) value. So if you wanted to download last Tuesday's daily license file:
60
+
61
+ ```
62
+ database = ULS::Retriever.amateur_radio.daily(:licenses, 2)
63
+ ```
64
+
65
+ ##### Parsing Databases
66
+
67
+ After you have an available database to work with, you may start interacting with the records within it:
68
+
69
+ ```
70
+ database.name_and_addresses.each_record do |record|
71
+ # do something
72
+ end
73
+ ```
74
+
75
+ When you're done with a database, you can clean up the extracted files using:
76
+
77
+ ```
78
+ database.clean
79
+ ```
80
+
81
+ If you're completely done with this version of the database then this command will remove the zip file:
82
+
83
+ ```
84
+ database.delete
85
+ ```
86
+
87
+ If you want to do both:
88
+
89
+ ```
90
+ database.delete!
91
+ ```
92
+
93
+ The above will not only remove all the extracted files but also delete the compressed file.
94
+
95
+ There may be instances where you want to keep the source files around. A database can be reconstructed from the filesystem by calling:
96
+
97
+ ```
98
+ ULS::Database.new('/path/to/zip/or/extracted/directory')
99
+ ```
100
+
101
+ You may pass the path to the zip file, or the already extracted directory, to interact with the records within.
102
+
103
+ ## Development
104
+
105
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
106
+
107
+ ## Contributing
108
+
109
+ Bug reports and pull requests are welcome on GitHub at https://github.com/gdubin/uls. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
110
+
111
+ ## License
112
+
113
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
114
+
115
+ ## Code of Conduct
116
+
117
+ Everyone interacting in the ULS project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/uls/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "uls"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,22 @@
1
+ module ULS
2
+ module Codes
3
+ class ApplicantType < Base
4
+ define_uls_code 'B', description: 'Amateur Club'
5
+ define_uls_code 'C', description: 'Corporation'
6
+ define_uls_code 'D', description: 'General Partnership'
7
+ define_uls_code 'E', description: 'Limited Partnership'
8
+ define_uls_code 'F', description: 'Limited Liability Partnership'
9
+ define_uls_code 'G', description: 'Government Entity'
10
+ define_uls_code 'H', description: 'Other'
11
+ define_uls_code 'I', description: 'Individual'
12
+ define_uls_code 'J', description: 'Joint Venture'
13
+ define_uls_code 'L', description: 'Limited Liability Company'
14
+ define_uls_code 'M', description: 'Military Recreation'
15
+ define_uls_code 'O', description: 'Consortium'
16
+ define_uls_code 'P', description: 'Partnership'
17
+ define_uls_code 'R', description: 'RACES'
18
+ define_uls_code 'T', description: 'Trust'
19
+ define_uls_code 'U', description: 'Unincorporated Association'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,43 @@
1
+ module ULS
2
+ module Codes
3
+ # A base class to handle the various codes/descriptions that the ULS database
4
+ # uses throughout their records.
5
+ class Base
6
+ attr_accessor :code
7
+
8
+ class << self
9
+ def define_uls_code(code, description:)
10
+ possible_codes[code] = description
11
+
12
+ # Takes the description and turns it into a question mark
13
+ # method for quick testing. For example:
14
+ #
15
+ # 'Active' -> :active?
16
+ # 'Pending Legal Status' -> :pending_legal_status?
17
+ question_method = description.downcase
18
+ question_method.tr!(' ', '_')
19
+ question_method += '?'
20
+ define_method question_method.to_sym do
21
+ self.code.eql?(code)
22
+ end
23
+ end
24
+
25
+ def possible_codes
26
+ @possible_codes ||= {}
27
+ end
28
+ end
29
+
30
+ def initialize(code = nil)
31
+ return if code.nil?
32
+
33
+ raise ArgumentError, "'#{code}' is an invalid code." unless self.class.possible_codes.include?(code)
34
+
35
+ self.code = code
36
+ end
37
+
38
+ def description
39
+ self.class.possible_codes[code] unless code.nil?
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ module ULS
2
+ module Codes
3
+ class EntityType < Base
4
+ define_uls_code 'CE', description: 'Transferee Contact'
5
+ define_uls_code 'CL', description: 'Licensee Contact'
6
+ define_uls_code 'CR', description: 'Assignor or Transferor Contact'
7
+ define_uls_code 'CS', description: 'Lessee Contact'
8
+ define_uls_code 'E', description: 'Transferee'
9
+ define_uls_code 'L', description: 'Licensee or Assignee'
10
+ define_uls_code 'O', description: 'Owner'
11
+ define_uls_code 'R', description: 'Assignor or Transferor'
12
+ define_uls_code 'S', description: 'Lessee'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module ULS
2
+ module Codes
3
+ class LicenseStatus < Base
4
+ define_uls_code 'A', description: 'Active'
5
+ define_uls_code 'C', description: 'Canceled'
6
+ define_uls_code 'E', description: 'Expired'
7
+ define_uls_code 'L', description: 'Pending Legal Status'
8
+ define_uls_code 'P', description: 'Parent Station Canceled'
9
+ define_uls_code 'T', description: 'Terminated'
10
+ define_uls_code 'X', description: 'Term Pending'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module ULS
2
+ module Codes
3
+ class Status < Base
4
+ # Code value is intentionally blank. That's how the FCC represents
5
+ # an active status: blank or null.
6
+ define_uls_code '', description: 'Active'
7
+ define_uls_code 'X', description: 'Termination Pending'
8
+ define_uls_code 'T', description: 'Terminated'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'tmpdir'
2
+
3
+ module ULS
4
+ class Configuration
5
+ attr_accessor :tmpdir
6
+
7
+ def initialize
8
+ @tmpdir = Dir.tmpdir
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ module ULS
2
+ # Used to interact with the FCC DAT file.
3
+ class DatFile
4
+ attr_accessor :path
5
+
6
+ def initialize(path)
7
+ self.path = path
8
+ end
9
+
10
+ def each_line(&_block)
11
+ return unless File.exist?(path)
12
+
13
+ File.foreach(path) { |line| yield(line) }
14
+ end
15
+
16
+ def each_record(&_block)
17
+ return unless File.exist?(path)
18
+
19
+ File.foreach(path) do |line|
20
+ record = line_to_record(line)
21
+ yield(record)
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def record_class(record_type)
28
+ case record_type
29
+ when 'EN'
30
+ Records::Entity
31
+ when 'HD'
32
+ Records::FormPrimary
33
+ when 'AD'
34
+ Records::FormSecondary
35
+ end
36
+ end
37
+
38
+ def line_to_record(line)
39
+ klass = record_class(line[0..1])
40
+ klass.new(line) unless klass.nil?
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,72 @@
1
+ require 'zip'
2
+
3
+ module ULS
4
+ # Used to interact with the FCC 'database' files. These files are compressed
5
+ # files containing several individual pipe-delimited data files.
6
+ class Database
7
+ attr_accessor :compressed_file
8
+
9
+ # Use to construct an interface to the Database zipfile/directory
10
+ # retrieved from the FCC. The filename may either be a zip file,
11
+ # in which case it will be extracted to the temporary directory, or
12
+ # may be an already extract directory containing the various 'database'
13
+ # files.
14
+ def initialize(filename)
15
+ path = Pathname.new(filename)
16
+ if path.directory?
17
+ @extracted_path = filename
18
+ else
19
+ @compressed_file = filename
20
+ end
21
+ end
22
+
23
+ def name_and_addresses
24
+ extract unless extracted?
25
+
26
+ DatFile.new("#{extracted_path}/EN.dat")
27
+ end
28
+
29
+ def form_primary
30
+ extract unless extracted?
31
+
32
+ DatFile.new("#{extracted_path}/HD.dat")
33
+ end
34
+
35
+ def form_secondary
36
+ extract unless extracted?
37
+
38
+ DatFile.new("#{extracted_path}/AD.dat")
39
+ end
40
+
41
+ def extracted_path
42
+ @extracted_path ||= "#{ULS.configuration.tmpdir}/uls_#{Time.now.to_i}"
43
+ end
44
+
45
+ def extracted?
46
+ Dir.exist?(extracted_path)
47
+ end
48
+
49
+ def extract
50
+ FileUtils.mkdir_p(extracted_path)
51
+ Zip::File.open(compressed_file) do |opened_file|
52
+ opened_file.each do |entry|
53
+ entry.extract("#{extracted_path}/#{entry.name}")
54
+ end
55
+ end
56
+ end
57
+
58
+ def clean
59
+ File.rmdir(extracted_path)
60
+ end
61
+
62
+ def delete
63
+ File.delete(compressed_file)
64
+ end
65
+
66
+ def delete!
67
+ clean if extracted?
68
+
69
+ delete
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,80 @@
1
+ require 'date'
2
+
3
+ module ULS
4
+ module Records
5
+ # The base class for all ULS records we'll be parsing. This handles some
6
+ # of the helper methods for defining attributes and actually performing
7
+ # the parsing after the record is defined.
8
+ class Base
9
+ DEFAULT_ATTRIBUTE_OPTIONS = { type: :string }.freeze
10
+ FIELD_SEPARATOR = '|'.freeze
11
+ BLANK_RE = /\A[[:space:]]*\z/ # h/t Rails .blank? implementation
12
+
13
+ class << self
14
+ def uls_field_accessor(symbol, options = {})
15
+ field = { attribute: symbol }
16
+ field.merge!(options)
17
+ field = DEFAULT_ATTRIBUTE_OPTIONS.merge(field)
18
+
19
+ fields << field
20
+
21
+ attr_accessor symbol
22
+ end
23
+
24
+ def fields
25
+ @fields ||= []
26
+ end
27
+ end
28
+
29
+ def initialize(line = nil)
30
+ return if line.nil?
31
+
32
+ from_row(line)
33
+ end
34
+
35
+ def from_row(line)
36
+ values = line.split(FIELD_SEPARATOR)
37
+ values.shift
38
+
39
+
40
+ values.each_with_index do |value, index|
41
+ break if index >= self.class.fields.size
42
+
43
+ field = self.class.fields[index]
44
+ setter = "#{field[:attribute]}="
45
+ value = convert(value, field[:type])
46
+
47
+ send(setter, value)
48
+ end
49
+ end
50
+
51
+ protected
52
+
53
+ def convert(value, type)
54
+ begin
55
+ return type.new(value) if type.respond_to?(:ancestors) && type.ancestors.include?(ULS::Codes::Base)
56
+ rescue ArgumentError => e
57
+ return nil
58
+ end
59
+
60
+ # The FCC uses blank values to represent certain codes so this check is after
61
+ # any code conversion that may occur. Beyond the codes, we want to set blanks
62
+ # to nil since that's what they represent normally.
63
+ return nil if blank?(value)
64
+
65
+ case type
66
+ when :numeric
67
+ value.to_i
68
+ when :date
69
+ Date.strptime(value, '%m/%d/%Y')
70
+ else
71
+ value
72
+ end
73
+ end
74
+
75
+ def blank?(value)
76
+ return true if value.nil? || BLANK_RE.match?(value)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,32 @@
1
+ module ULS
2
+ module Records
3
+ class Entity < Base
4
+ uls_field_accessor :unique_system_identifier, type: :numeric
5
+ uls_field_accessor :file_number
6
+ uls_field_accessor :ebf_number
7
+ uls_field_accessor :call_sign
8
+ uls_field_accessor :entity_type, type: ULS::Codes::EntityType
9
+ uls_field_accessor :licensee_id
10
+ uls_field_accessor :entity_name
11
+ uls_field_accessor :first_name
12
+ uls_field_accessor :middle_name
13
+ uls_field_accessor :last_name
14
+ uls_field_accessor :suffix
15
+ uls_field_accessor :phone
16
+ uls_field_accessor :fax
17
+ uls_field_accessor :email
18
+ uls_field_accessor :street_address
19
+ uls_field_accessor :city
20
+ uls_field_accessor :state
21
+ uls_field_accessor :zipcode
22
+ uls_field_accessor :po_box
23
+ uls_field_accessor :attention_line
24
+ uls_field_accessor :sgin
25
+ uls_field_accessor :frn
26
+ uls_field_accessor :applicant_type, type: ULS::Codes::ApplicantType
27
+ uls_field_accessor :applicant_type_other
28
+ uls_field_accessor :status, type: ULS::Codes::Status
29
+ uls_field_accessor :status_date, type: :date
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,56 @@
1
+ module ULS
2
+ module Records
3
+ # This represents the main form 605 data that carries over to the license.
4
+ class FormPrimary < Base
5
+ uls_field_accessor :unique_system_identifier, type: :numeric
6
+ uls_field_accessor :uls_file_number
7
+ uls_field_accessor :ebf_number
8
+ uls_field_accessor :call_sign
9
+ uls_field_accessor :license_status, type: ULS::Codes::LicenseStatus
10
+ uls_field_accessor :radio_service_code
11
+ uls_field_accessor :grant_date, type: :date
12
+ uls_field_accessor :expired_date, type: :date
13
+ uls_field_accessor :cancellation_date, type: :date
14
+ uls_field_accessor :eligibility_rule_num
15
+ uls_field_accessor :applicant_type_code_reserved
16
+ uls_field_accessor :alien
17
+ uls_field_accessor :alien_government
18
+ uls_field_accessor :alien_corporation
19
+ uls_field_accessor :alien_officer
20
+ uls_field_accessor :alien_control
21
+ uls_field_accessor :revoked
22
+ uls_field_accessor :convicted
23
+ uls_field_accessor :adjudged
24
+ uls_field_accessor :involved_reserved
25
+ uls_field_accessor :common_carrier
26
+ uls_field_accessor :non_common_carrier
27
+ uls_field_accessor :private_comm
28
+ uls_field_accessor :fixed
29
+ uls_field_accessor :mobile
30
+ uls_field_accessor :radiolocation
31
+ uls_field_accessor :satellite
32
+ uls_field_accessor :developmental_or_sta
33
+ uls_field_accessor :interconnected_service
34
+ uls_field_accessor :certifier_first_name
35
+ uls_field_accessor :certifier_mi
36
+ uls_field_accessor :certifier_last_name
37
+ uls_field_accessor :certifier_suffix
38
+ uls_field_accessor :certifier_title
39
+ uls_field_accessor :gender
40
+ uls_field_accessor :african_american
41
+ uls_field_accessor :native_american
42
+ uls_field_accessor :hawaiian
43
+ uls_field_accessor :asian
44
+ uls_field_accessor :white
45
+ uls_field_accessor :ethnicity
46
+ uls_field_accessor :effective_date
47
+ uls_field_accessor :last_action_date
48
+ uls_field_accessor :auction_id, type: :numeric
49
+ uls_field_accessor :reg_stat_broad_serv
50
+ uls_field_accessor :band_manager
51
+ uls_field_accessor :type_serv_broad_serv
52
+ uls_field_accessor :alien_ruling
53
+ uls_field_accessor :licensee_name_change
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,36 @@
1
+ module ULS
2
+ module Records
3
+ # This represents the Main Form 605 data that does not carry over to the license.
4
+ class FormSecondary < Base
5
+ uls_field_accessor :unique_system_identifier, type: :numeric
6
+ uls_field_accessor :uls_file_number
7
+ uls_field_accessor :ebf_number
8
+ uls_field_accessor :application_purpose
9
+ uls_field_accessor :application_status
10
+ uls_field_accessor :application_fee_exempt
11
+ uls_field_accessor :regulatory_fee_exempt
12
+ uls_field_accessor :source
13
+ uls_field_accessor :requested_expiration_date_mmdd
14
+ uls_field_accessor :receipt_date
15
+ uls_field_accessor :notification_code
16
+ uls_field_accessor :notification_date
17
+ uls_field_accessor :expanding_area_or_contour
18
+ uls_field_accessor :change_type
19
+ uls_field_accessor :original_application_purpose
20
+ uls_field_accessor :requesting_a_waiver
21
+ uls_field_accessor :how_many_waivers_requested, type: :numeric
22
+ uls_field_accessor :any_attachments
23
+ uls_field_accessor :number_of_requested_sids, type: :numeric
24
+ uls_field_accessor :fee_control_num
25
+ uls_field_accessor :date_entered
26
+ uls_field_accessor :reason
27
+ uls_field_accessor :frequency_coordination_indicat
28
+ uls_field_accessor :emergency_sta
29
+ uls_field_accessor :overall_change_type
30
+ uls_field_accessor :slow_growth_ind
31
+ uls_field_accessor :previous_waiver
32
+ uls_field_accessor :waiver_deferral_fee
33
+ uls_field_accessor :has_term_pending_ind
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,89 @@
1
+ require 'date'
2
+ require 'open-uri'
3
+
4
+ module ULS
5
+ class Retriever
6
+ APPLICATION_PREFIX = 'a'.freeze
7
+ LICENSE_PREFIX = 'l'.freeze
8
+
9
+ SERVICES = {
10
+ amateur_radio: {
11
+ filename: {
12
+ daily: 'am',
13
+ weekly: 'amat'
14
+ },
15
+ records: {
16
+ applications: APPLICATION_PREFIX,
17
+ licenses: LICENSE_PREFIX
18
+ }
19
+ }
20
+ }.freeze
21
+
22
+ # Different base URLs whether we're looking for the daily updates or the full
23
+ # weekly downloads.
24
+ DAILY_BASE_URL = 'ftp://wirelessftp.fcc.gov/pub/uls/daily'.freeze
25
+ WEEKLY_BASE_URL = 'ftp://wirelessftp.fcc.gov/pub/uls/complete'.freeze
26
+
27
+ attr_accessor :service
28
+
29
+ def self.amateur_radio
30
+ Retriever.new(:amateur_radio)
31
+ end
32
+
33
+ def initialize(service)
34
+ raise ArgumentError, "Invalid service #{service}" unless services.include?(service)
35
+
36
+ self.service = service
37
+ end
38
+
39
+ def weekly_url(type)
40
+ raise ArgumentError, "Invalid type #{type}" unless types.include?(type)
41
+
42
+ prefix = SERVICES[service][:records][type]
43
+ name = SERVICES[service][:filename][:weekly]
44
+
45
+ "#{WEEKLY_BASE_URL}/#{prefix}_#{name}.zip"
46
+ end
47
+
48
+ def weekly(type)
49
+ url = weekly_url(type)
50
+ download(url)
51
+ end
52
+
53
+ def daily_url(type, wday = Date.new.prev_day.wday)
54
+ raise ArgumentError, "Invalid type #{type}" unless types.include?(type)
55
+
56
+ abbreviated_day = Date::ABBR_DAYNAMES[wday].downcase
57
+ prefix = SERVICES[service][:records][type]
58
+ name = SERVICES[service][:filename][:daily]
59
+
60
+ "#{DAILY_BASE_URL}/#{prefix}_#{name}_#{abbreviated_day}.zip"
61
+ end
62
+
63
+ def daily(type, wday = Date.new.prev_day.wday)
64
+ url = daily_url(type, wday)
65
+ download(url)
66
+ end
67
+
68
+ def types
69
+ SERVICES[service][:records].keys
70
+ end
71
+
72
+ def services
73
+ SERVICES.keys
74
+ end
75
+
76
+ protected
77
+
78
+ def download(url)
79
+ filename = url.split('/').last
80
+ path = "#{ULS.configuration.tmpdir}/#{filename}"
81
+
82
+ File.open(path, 'w') do |file|
83
+ IO.copy_stream(open(url), file)
84
+ end
85
+
86
+ path
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,3 @@
1
+ module ULS
2
+ VERSION = "0.2.0"
3
+ end
data/lib/uls.rb ADDED
@@ -0,0 +1,34 @@
1
+ require 'uls/version'
2
+ require 'uls/configuration'
3
+ require 'uls/database'
4
+ require 'uls/dat_file'
5
+ require 'uls/retriever'
6
+ require 'uls/codes/base'
7
+ require 'uls/codes/status'
8
+ require 'uls/codes/license_status'
9
+ require 'uls/codes/applicant_type'
10
+ require 'uls/codes/entity_type'
11
+ require 'uls/records/base'
12
+ require 'uls/records/form_primary'
13
+ require 'uls/records/form_secondary'
14
+ require 'uls/records/entity'
15
+
16
+ # This gem is for parsing the FCC ULS database files available from the FCC website.
17
+ # For additional details, visit http://github.com/gdubin/uls
18
+ module ULS
19
+ class << self
20
+ attr_writer :configuration
21
+
22
+ def configuration
23
+ @configuration ||= Configuration.new
24
+ end
25
+ end
26
+
27
+ def self.reset
28
+ @configuration = Configuration.new
29
+ end
30
+
31
+ def self.configure(&_block)
32
+ yield(configuration)
33
+ end
34
+ end
data/uls.gemspec ADDED
@@ -0,0 +1,43 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "uls/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "uls"
8
+ spec.version = ULS::VERSION
9
+ spec.authors = ["Garth Dubin"]
10
+ spec.email = ["garth@codejunkie.org"]
11
+
12
+ spec.summary = %q{Parsing library for FCC ULS databases.}
13
+ spec.description = %q{Handles the parsing of the FCC ULS databases.}
14
+ spec.homepage = "http://github.com/gdubin/uls"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against " \
23
+ "public gem pushes."
24
+ end
25
+
26
+ # Specify which files should be added to the gem when it is released.
27
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
29
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_development_dependency "bundler", ">= 2.2.33"
36
+ spec.add_development_dependency "rake", "~> 12.3.3"
37
+ spec.add_development_dependency "rspec", "~> 3.0"
38
+ spec.add_development_dependency "pry", "~> 0.12.2"
39
+ spec.add_development_dependency "codecov", "~> 0.6.0"
40
+ spec.add_development_dependency "timecop", "~> 0.9.5"
41
+
42
+ spec.add_runtime_dependency 'rubyzip', '~> 2.3.0'
43
+ end
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uls
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Garth Dubin
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-06-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.2.33
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 2.2.33
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 12.3.3
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 12.3.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.12.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.12.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: codecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.6.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.6.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: timecop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.9.5
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.9.5
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubyzip
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 2.3.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 2.3.0
111
+ description: Handles the parsing of the FCC ULS databases.
112
+ email:
113
+ - garth@codejunkie.org
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".travis.yml"
121
+ - CODE_OF_CONDUCT.md
122
+ - Gemfile
123
+ - Gemfile.lock
124
+ - LICENSE.txt
125
+ - README.md
126
+ - Rakefile
127
+ - bin/console
128
+ - bin/setup
129
+ - lib/uls.rb
130
+ - lib/uls/codes/applicant_type.rb
131
+ - lib/uls/codes/base.rb
132
+ - lib/uls/codes/entity_type.rb
133
+ - lib/uls/codes/license_status.rb
134
+ - lib/uls/codes/status.rb
135
+ - lib/uls/configuration.rb
136
+ - lib/uls/dat_file.rb
137
+ - lib/uls/database.rb
138
+ - lib/uls/records/base.rb
139
+ - lib/uls/records/entity.rb
140
+ - lib/uls/records/form_primary.rb
141
+ - lib/uls/records/form_secondary.rb
142
+ - lib/uls/retriever.rb
143
+ - lib/uls/version.rb
144
+ - uls.gemspec
145
+ homepage: http://github.com/gdubin/uls
146
+ licenses:
147
+ - MIT
148
+ metadata:
149
+ allowed_push_host: https://rubygems.org
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ required_rubygems_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ requirements: []
165
+ rubygems_version: 3.3.3
166
+ signing_key:
167
+ specification_version: 4
168
+ summary: Parsing library for FCC ULS databases.
169
+ test_files: []