publicstorage 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91f7ec4d785ae72a4a91766044921b18fbb72bd794df5ef84396d691d8c8e20d
4
- data.tar.gz: b34ad3956eb8b9983aa5c02a67f236c4ba3caf42bfa676a14993e0452fe95992
3
+ metadata.gz: 5f5319690995d8d7f216e02fa0ead21f9c3bfc136e2e9ed8d46333f840909474
4
+ data.tar.gz: f0da92039ca2c9dd5dbf1256a5e3df2cffa304763ee766229e0175bfcf817047
5
5
  SHA512:
6
- metadata.gz: 17c85cd24def5db02b92ad2a8e8c4e2919344868102f6a16dea871af3f162152f8211dc0207425d81a905d925c90b3f43ecf0daf30b8d56fce77dfe4b9e8b6fa
7
- data.tar.gz: f1ae2c946fbaa636dab672ad84af48962dd3635129bca987224c0cc9c739af520d49d6b5d1fd9cb15d4684c22618816912e64c71585f1dec227dc0ed2eb84c59
6
+ metadata.gz: 723e933f0da23b57b731e9123a7a412eb9984e7f54897a77304d1f0753cc274f1ffbf68711b59d25e4f9283f38ca4e3ee0aa8ffc9e606cdf26f0a92d4581e634
7
+ data.tar.gz: 5149248cc1fa272c78cc9ac8666bfe52501b23228a2dba66533c94c25ca5294fc9e31717fae635ea46361253428197c0007f1f577fca0e34cde15f49021bb4bf
data/README.md CHANGED
@@ -20,24 +20,20 @@ require 'publicstorage'
20
20
  sitemap = PublicStorage::Facility.sitemap
21
21
  sitemap.links.each do |link|
22
22
  url = link.loc
23
+ facility = ExtraSpace::Facility.fetch(url:)
23
24
 
24
- facility = PublicStorage::Facility.fetch(url:)
25
-
26
- puts "Street: #{facility.address.street}"
27
- puts "City: #{facility.address.city}"
28
- puts "State: #{facility.address.state}"
29
- puts "ZIP: #{facility.address.zip}"
30
- puts "Latitude: #{facility.geocode.latitude}"
31
- puts "Longitude: #{facility.geocode.longitude}"
32
- puts
25
+ puts facility.text
33
26
 
34
27
  facility.prices.each do |price|
35
- puts "ID: #{price.id}"
36
- puts "Width: #{price.dimensions.width}"
37
- puts "Depth: #{price.dimensions.depth}"
38
- puts "SQFT: #{price.dimensions.sqft}"
39
- puts "Rates: $#{price.rates.street} (street) / $#{price.rates.web} (web)"
40
- puts
28
+ puts price.text
41
29
  end
30
+
31
+ puts
42
32
  end
43
33
  ```
34
+
35
+ ## CLI
36
+
37
+ ```bash
38
+ publicstorage crawl
39
+ ```
data/exe/publicstorage ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'publicstorage'
5
+
6
+ cli = PublicStorage::CLI.new
7
+ cli.parse
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PublicStorage
4
- # Represents an address associated with a facility.
4
+ # The address (street + city + state + zip) of a facility.
5
5
  class Address
6
6
  # @attribute [rw] street
7
7
  # @return [String]
@@ -41,6 +41,11 @@ module PublicStorage
41
41
  "#<#{self.class.name} #{props.join(' ')}>"
42
42
  end
43
43
 
44
+ # @return [String]
45
+ def text
46
+ "#{street}, #{city}, #{state} #{zip}"
47
+ end
48
+
44
49
  # @param data [Hash]
45
50
  #
46
51
  # @return [Address]
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module PublicStorage
6
+ # Used when interacting with the library from the command line interface (CLI).
7
+ #
8
+ # Usage:
9
+ #
10
+ # cli = PublicStorage::CLI.new
11
+ # cli.parse
12
+ class CLI
13
+ module Code
14
+ OK = 0
15
+ ERROR = 1
16
+ end
17
+
18
+ # @param argv [Array<String>]
19
+ def parse(argv = ARGV)
20
+ parser.parse!(argv)
21
+ command = argv.shift
22
+
23
+ case command
24
+ when 'crawl' then crawl
25
+ else
26
+ warn("unsupported command=#{command.inspect}")
27
+ exit(Code::ERROR)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def crawl
34
+ PublicStorage::Facility.crawl
35
+ exit(Code::OK)
36
+ end
37
+
38
+ def help(options)
39
+ puts(options)
40
+ exit(Code::OK)
41
+ end
42
+
43
+ def version
44
+ puts(VERSION)
45
+ exit(Code::OK)
46
+ end
47
+
48
+ # @return [OptionParser]
49
+ def parser
50
+ OptionParser.new do |options|
51
+ options.banner = 'usage: PublicStorage [options] <command> [<args>]'
52
+
53
+ options.on('-h', '--help', 'help') { help(options) }
54
+ options.on('-v', '--version', 'version') { version }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -30,7 +30,7 @@ module PublicStorage
30
30
  # @return [HTTP::Response]
31
31
  def fetch(url:)
32
32
  response = HTTP.get(url)
33
- raise FetchError(url:, response: response.flush) unless response.status.ok?
33
+ raise FetchError.new(url:, response: response.flush) unless response.status.ok?
34
34
 
35
35
  response
36
36
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PublicStorage
4
- # The dimensions associated with a price.
4
+ # The dimensions (width + depth + sqft) of a price.
5
5
  class Dimensions
6
6
  SELECTOR = '.unit-size'
7
7
 
@@ -36,6 +36,11 @@ module PublicStorage
36
36
  "#<#{self.class.name} #{props.join(' ')}>"
37
37
  end
38
38
 
39
+ # @return [String] e.g. "10' × 10' (100 sqft)"
40
+ def text
41
+ "#{format('%g', @width)}' × #{format('%g', @depth)}' (#{@sqft} sqft)"
42
+ end
43
+
39
44
  # @param element [Nokogiri::XML::Element]
40
45
  #
41
46
  # @return [Dimensions]
@@ -6,6 +6,17 @@ module PublicStorage
6
6
  SITEMAP_URL = 'https://www.publicstorage.com/sitemap_0-product.xml'
7
7
 
8
8
  PRICE_SELECTOR = '.units-results-section .unit-list-group .unit-list-item'
9
+ LD_SELECTOR = 'script[type="application/ld+json"]'
10
+
11
+ ID_REGEX = /(?<id>\d+)\.html/
12
+
13
+ # @attribute [rw] id
14
+ # @return [Integer]
15
+ attr_accessor :id
16
+
17
+ # @attribute [rw] name
18
+ # @return [String]
19
+ attr_accessor :name
9
20
 
10
21
  # @attribute [rw] address
11
22
  # @return [Address]
@@ -19,25 +30,6 @@ module PublicStorage
19
30
  # @return [Array<Price>]
20
31
  attr_accessor :prices
21
32
 
22
- # @param address [Address]
23
- # @param geocode [Geocode]
24
- # @param prices [Array<Price>]
25
- def initialize(address:, geocode:, prices:)
26
- @address = address
27
- @geocode = geocode
28
- @prices = prices
29
- end
30
-
31
- # @return [String]
32
- def inspect
33
- props = [
34
- "address=#{@address.inspect}",
35
- "geocode=#{@geocode.inspect}",
36
- "prices=#{@prices.inspect}"
37
- ]
38
- "#<#{self.class.name} #{props.join(' ')}>"
39
- end
40
-
41
33
  # @return [Sitemap]
42
34
  def self.sitemap
43
35
  Sitemap.fetch(url: SITEMAP_URL)
@@ -55,14 +47,65 @@ module PublicStorage
55
47
  #
56
48
  # @return [Facility]
57
49
  def self.parse(document:)
58
- data = JSON.parse(document.at_css('script[type="application/ld+json"]').text)
59
- item = data.find { |entry| entry['@type'] == 'SelfStorage' }
60
- address = Address.parse(data: item['address'])
61
- geocode = Geocode.parse(data: item['geo'])
50
+ data = parse_ld(document:)
51
+ id = Integer(data['url'].match(ID_REGEX)[:id])
52
+ name = data['name']
53
+ address = Address.parse(data: data['address'])
54
+ geocode = Geocode.parse(data: data['geo'])
62
55
 
63
56
  prices = document.css(PRICE_SELECTOR).map { |element| Price.parse(element:) }
64
57
 
65
- new(address:, geocode:, prices:)
58
+ new(id:, name:, address:, geocode:, prices:)
59
+ end
60
+
61
+ # @param document [NokoGiri::XML::Document]
62
+ #
63
+ # @return [Hash]
64
+ def self.parse_ld(document:)
65
+ JSON.parse(document.at_css(LD_SELECTOR).text).find { |entry| entry['@type'] == 'SelfStorage' }
66
+ end
67
+
68
+ def self.crawl
69
+ sitemap.links.each do |link|
70
+ url = link.loc
71
+
72
+ facility = fetch(url:)
73
+ puts facility.text
74
+
75
+ facility.prices.each do |price|
76
+ puts price.text
77
+ end
78
+
79
+ puts
80
+ end
81
+ end
82
+
83
+ # @param id [String]
84
+ # @param name [String]
85
+ # @param address [Address]
86
+ # @param geocode [Geocode]
87
+ # @param prices [Array<Price>]
88
+ def initialize(id:, name:, address:, geocode:, prices:)
89
+ @id = id
90
+ @name = name
91
+ @address = address
92
+ @geocode = geocode
93
+ @prices = prices
94
+ end
95
+
96
+ # @return [String]
97
+ def inspect
98
+ props = [
99
+ "address=#{@address.inspect}",
100
+ "geocode=#{@geocode.inspect}",
101
+ "prices=#{@prices.inspect}"
102
+ ]
103
+ "#<#{self.class.name} #{props.join(' ')}>"
104
+ end
105
+
106
+ # @return [String]
107
+ def text
108
+ "#{@id} | #{@name} | #{@address.text} | #{@geocode.text}"
66
109
  end
67
110
  end
68
111
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PublicStorage
4
- # A geocode associated with a facility.
4
+ # The geocode (latitude + longitude) of a facility.
5
5
  class Geocode
6
6
  # @attribute [rw] latitude
7
7
  # @return [Float]
@@ -27,6 +27,11 @@ module PublicStorage
27
27
  "#<#{self.class.name} #{props.join(' ')}>"
28
28
  end
29
29
 
30
+ # @return [String]
31
+ def text
32
+ "#{@latitude},#{@longitude}"
33
+ end
34
+
30
35
  # @param data [Hash]
31
36
  #
32
37
  # @return [Geocode]
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PublicStorage
4
- # A price associated with a unit.
4
+ # The price (id + dimensions + rate) for a facility
5
5
  class Price
6
6
  # @attribute [rw] id
7
7
  # @return [String]
@@ -34,6 +34,11 @@ module PublicStorage
34
34
  "#<#{self.class.name} #{props.join(' ')}>"
35
35
  end
36
36
 
37
+ # @return [String] e.g. "123 | 5' × 5' (25 sqft) | $100 (street) / $90 (web)"
38
+ def text
39
+ "#{@id} | #{@dimensions.text} | #{@rates.text}"
40
+ end
41
+
37
42
  # @param element [Nokogiri::XML::Element]
38
43
  #
39
44
  # @return [Price]
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PublicStorage
4
- # The rates associated with a price
4
+ # The rates (street + web) for a facility
5
5
  class Rates
6
6
  STREET_SELECTOR = '.unit-prices .unit-pricing .unit-strike-through-price'
7
7
  WEB_SELECTOR = '.unit-prices .unit-pricing .unit-price'
@@ -30,6 +30,11 @@ module PublicStorage
30
30
  "#<#{self.class.name} #{props.join(' ')}>"
31
31
  end
32
32
 
33
+ # @return [String] e.g. "$80 (street) | $60 (web)"
34
+ def text
35
+ "$#{@street} (street) | $#{@web} (web)"
36
+ end
37
+
33
38
  # @param element [Nokogiri::XML::Element]
34
39
  #
35
40
  # @return [Rates]
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PublicStorage
4
- # A sitemap.
4
+ # A sitemap on publicstorage.com.
5
+ #
6
+ # e.g. https://www.publicstorage.com/sitemap_0-product.xml
5
7
  class Sitemap
6
8
  # @attribute [rw] links
7
9
  # @return [Array<Link>]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PublicStorage
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/publicstorage.rb CHANGED
@@ -6,6 +6,7 @@ require 'zeitwerk'
6
6
 
7
7
  loader = Zeitwerk::Loader.for_gem
8
8
  loader.inflector.inflect 'publicstorage' => 'PublicStorage'
9
+ loader.inflector.inflect 'cli' => 'CLI'
9
10
  loader.setup
10
11
 
11
12
  module PublicStorage
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: publicstorage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
11
  date: 2024-11-27 00:00:00.000000000 Z
@@ -69,7 +69,8 @@ dependencies:
69
69
  description: Uses HTTP.rb to scrape publicstorage.com.
70
70
  email:
71
71
  - kevin@ksylvest.com
72
- executables: []
72
+ executables:
73
+ - publicstorage
73
74
  extensions: []
74
75
  extra_rdoc_files: []
75
76
  files:
@@ -77,8 +78,10 @@ files:
77
78
  - README.md
78
79
  - bin/console
79
80
  - bin/setup
81
+ - exe/publicstorage
80
82
  - lib/publicstorage.rb
81
83
  - lib/publicstorage/address.rb
84
+ - lib/publicstorage/cli.rb
82
85
  - lib/publicstorage/crawler.rb
83
86
  - lib/publicstorage/dimensions.rb
84
87
  - lib/publicstorage/facility.rb
@@ -96,7 +99,7 @@ metadata:
96
99
  homepage_uri: https://github.com/ksylvest/publicstorage
97
100
  source_code_uri: https://github.com/ksylvest/publicstorage
98
101
  changelog_uri: https://github.com/ksylvest/publicstorage
99
- post_install_message:
102
+ post_install_message:
100
103
  rdoc_options: []
101
104
  require_paths:
102
105
  - lib
@@ -111,8 +114,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
114
  - !ruby/object:Gem::Version
112
115
  version: '0'
113
116
  requirements: []
114
- rubygems_version: 3.5.22
115
- signing_key:
117
+ rubygems_version: 3.5.23
118
+ signing_key:
116
119
  specification_version: 4
117
120
  summary: A crawler for PublicStorage.
118
121
  test_files: []