publicstorage 0.3.0 → 1.1.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: 34a6d2f42dac72b5aac92461444a57e40f3dd42ace468fc24f61ac62c81d09e1
4
- data.tar.gz: bbb5c737bcf5236e4236fe6d0b336497557f1e300938924b1eb9727c204a9ff1
3
+ metadata.gz: f797ad8918b9cea8da35957e83b8bbf56ab02f1bfae95ee24a0fe1a316d1117d
4
+ data.tar.gz: e2a976a36b0fc0d5edefde19c7234b778257341abc9c9507029bac2d6e31f1e3
5
5
  SHA512:
6
- metadata.gz: b1bed2e588707393e89bf67a416bc8f3622dda29e3c8b4754cfd015f224555530a628cce7cbb68e5c4787c745905493ae8c6e6efe14a8c6d84fae852f213474b
7
- data.tar.gz: e46336139bbf2f3054cff093e650b88c5e84b7bb0beaa876b5f833d192c641fa3e48449ca27a3af1215a2fdd9dafd284c937348951248e5704ca62668b43f11d
6
+ metadata.gz: eb338e5697de874ba118f47828d19ed3ed817cd136aac67372064fcea2dabde722c34cb39924b55e91bbbca3802b8469b7caf6a5fef5fa64abf559a2c2e38090
7
+ data.tar.gz: 8ed743ff4a0a59deefe92064d62345d6deaaceb860e597583758b8e7d18eac6f0db6b12f67ef0324212882ef1fea14009501b2ab0566d139241ae4d78d60f7eb
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PublicStorage
1
+ # Public Storage
2
2
 
3
3
  [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ksylvest/publicstorage/blob/main/LICENSE)
4
4
  [![RubyGems](https://img.shields.io/gem/v/publicstorage)](https://rubygems.org/gems/publicstorage)
@@ -6,6 +6,8 @@
6
6
  [![Yard](https://img.shields.io/badge/docs-site-blue.svg)](https://publicstorage.ksylvest.com)
7
7
  [![CircleCI](https://img.shields.io/circleci/build/github/ksylvest/publicstorage)](https://circleci.com/gh/ksylvest/publicstorage)
8
8
 
9
+ A Ruby library offering both a CLI and API for scraping [Public Storage](https://www.publicstorage.com/) self-storage facilities and prices.
10
+
9
11
  ## Installation
10
12
 
11
13
  ```bash
@@ -31,7 +33,7 @@ require 'publicstorage'
31
33
  sitemap = PublicStorage::Facility.sitemap
32
34
  sitemap.links.each do |link|
33
35
  url = link.loc
34
- facility = ExtraSpace::Facility.fetch(url:)
36
+ facility = PublicStorage::Facility.fetch(url:)
35
37
 
36
38
  puts facility.text
37
39
 
@@ -48,3 +50,7 @@ end
48
50
  ```bash
49
51
  publicstorage crawl
50
52
  ```
53
+
54
+ ```bash
55
+ publicstorage crawl "https://www.publicstorage.com/self-storage-ca-venice/120.html"
56
+ ```
@@ -21,7 +21,7 @@ module PublicStorage
21
21
  command = argv.shift
22
22
 
23
23
  case command
24
- when 'crawl' then crawl
24
+ when 'crawl' then crawl(*argv)
25
25
  else
26
26
  warn("unsupported command=#{command.inspect}")
27
27
  exit(Code::ERROR)
@@ -30,8 +30,9 @@ module PublicStorage
30
30
 
31
31
  private
32
32
 
33
- def crawl
34
- PublicStorage::Facility.crawl
33
+ # @url [String] optional
34
+ def crawl(url = nil)
35
+ Crawl.run(url: url)
35
36
  exit(Code::OK)
36
37
  end
37
38
 
@@ -52,6 +53,11 @@ module PublicStorage
52
53
 
53
54
  options.on('-h', '--help', 'help') { help(options) }
54
55
  options.on('-v', '--version', 'version') { version }
56
+
57
+ options.separator <<~COMMANDS
58
+ commands:
59
+ crawl [url]
60
+ COMMANDS
55
61
  end
56
62
  end
57
63
  end
@@ -12,7 +12,7 @@ module PublicStorage
12
12
  attr_accessor :timeout
13
13
 
14
14
  def initialize
15
- @user_agent = ENV.fetch('PUBLIC_STORAGE_USER_AGENT', "publicstorage.rb/#{VERSION}")
15
+ @user_agent = ENV.fetch('PUBLICSTORAGE_USER_AGENT', "publicstorage.rb/#{VERSION}")
16
16
  @timeout = Integer(ENV.fetch('PUBLICSTORAGE_TIMEOUT', 60))
17
17
  end
18
18
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PublicStorage
4
+ # Handles the crawl command via CLI.
5
+ class Crawl
6
+ def self.run(...)
7
+ new(...).run
8
+ end
9
+
10
+ # @param stdout [IO] optional
11
+ # @param stderr [IO] optional
12
+ # @param url [String] optional
13
+ def initialize(stdout: $stdout, stderr: $stderr, url: nil)
14
+ @stdout = stdout
15
+ @stderr = stderr
16
+ @url = url
17
+ end
18
+
19
+ def run
20
+ if @url
21
+ process(url: @url)
22
+ else
23
+ sitemap = Facility.sitemap
24
+ @stdout.puts("count=#{sitemap.links.count}")
25
+ @stdout.puts
26
+ sitemap.links.each { |link| process(url: link.loc) }
27
+ end
28
+ end
29
+
30
+ def process(url:)
31
+ @stdout.puts(url)
32
+ facility = Facility.fetch(url: url)
33
+ @stdout.puts(facility.text)
34
+ facility.prices.each { |price| @stdout.puts(price.text) }
35
+ @stdout.puts
36
+ rescue FetchError => e
37
+ @stderr.puts("url=#{url} error=#{e.message}")
38
+ end
39
+ end
40
+ end
@@ -5,15 +5,6 @@ module PublicStorage
5
5
  class Crawler
6
6
  HOST = 'https://www.publicstorage.com'
7
7
 
8
- # Raised for unexpected HTTP responses.
9
- class FetchError < StandardError
10
- # @param url [String]
11
- # @param response [HTTP::Response]
12
- def initialize(url:, response:)
13
- super("url=#{url} status=#{response.status.inspect} body=#{response.body.inspect}")
14
- end
15
- end
16
-
17
8
  # @param url [String]
18
9
  # @raise [FetchError]
19
10
  # @return [Nokogiri::HTML::Document]
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PublicStorage
4
- # The dimensions (width + depth + sqft) of a price.
4
+ # The dimensions (width + depth + height) of a price.
5
5
  class Dimensions
6
+ DEFAULT_HEIGHT = 8.0 # feet
7
+
6
8
  SELECTOR = '.unit-size'
7
9
 
8
10
  # @attribute [rw] depth
@@ -13,17 +15,17 @@ module PublicStorage
13
15
  # @return [Integer]
14
16
  attr_accessor :width
15
17
 
16
- # @attribute [rw] sqft
17
- # @return [Integer]
18
- attr_accessor :sqft
18
+ # @attribute [rw] height
19
+ # @return [Integer]
20
+ attr_accessor :height
19
21
 
20
22
  # @param depth [Float]
21
23
  # @param width [Float]
22
- # @param sqft [Integer]
23
- def initialize(depth:, width:, sqft:)
24
+ # @param height [Float]
25
+ def initialize(depth:, width:, height: DEFAULT_HEIGHT)
24
26
  @depth = depth
25
27
  @width = width
26
- @sqft = sqft
28
+ @height = height
27
29
  end
28
30
 
29
31
  # @return [String]
@@ -31,26 +33,35 @@ module PublicStorage
31
33
  props = [
32
34
  "depth=#{@depth.inspect}",
33
35
  "width=#{@width.inspect}",
34
- "sqft=#{@sqft.inspect}"
36
+ "height=#{@height.inspect}"
35
37
  ]
36
38
  "#<#{self.class.name} #{props.join(' ')}>"
37
39
  end
38
40
 
39
41
  # @return [String] e.g. "10' × 10' (100 sqft)"
40
42
  def text
41
- "#{format('%g', @width)}' × #{format('%g', @depth)}' (#{@sqft} sqft)"
43
+ "#{format('%g', @width)}' × #{format('%g', @depth)}' (#{sqft} sqft)"
44
+ end
45
+
46
+ # @return [Integer]
47
+ def sqft
48
+ Integer(@width * @depth)
49
+ end
50
+
51
+ # @return [Integer]
52
+ def cuft
53
+ Integer(@width * @depth * @height)
42
54
  end
43
55
 
44
- # @param element [Nokogiri::XML::Element]
56
+ # @param data [Hash]
45
57
  #
46
58
  # @return [Dimensions]
47
- def self.parse(element:)
48
- match = element.at(SELECTOR).text.match(/(?<depth>[\d\.]+)'x(?<width>[\d\.]+)'/)
59
+ def self.parse(data:)
60
+ match = data['dimension'].match(/(?<depth>[\d\.]+)'x(?<width>[\d\.]+)'/)
49
61
  depth = Float(match[:depth])
50
62
  width = Float(match[:width])
51
- sqft = Integer(depth * width)
52
63
 
53
- new(depth:, width:, sqft:)
64
+ new(depth:, width:, height: DEFAULT_HEIGHT)
54
65
  end
55
66
  end
56
67
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PublicStorage
4
+ # Raised for unexpected HTTP responses.
5
+ class FetchError < StandardError
6
+ # @param url [String]
7
+ # @param response [HTTP::Response]
8
+ def initialize(url:, response:)
9
+ super("url=#{url} status=#{response.status.inspect} body=#{response.body.inspect}")
10
+ end
11
+ end
12
+ end
@@ -3,6 +3,8 @@
3
3
  module PublicStorage
4
4
  # The price (id + dimensions + rate) for a facility
5
5
  class Price
6
+ GTM_SELECTOR = 'button[data-gtmdata]'
7
+
6
8
  # @attribute [rw] id
7
9
  # @return [String]
8
10
  attr_accessor :id
@@ -43,8 +45,10 @@ module PublicStorage
43
45
  #
44
46
  # @return [Price]
45
47
  def self.parse(element:)
46
- rates = Rates.parse(element:)
47
- dimensions = Dimensions.parse(element:)
48
+ data = JSON.parse(element.at(GTM_SELECTOR).attribute('data-gtmdata'))
49
+
50
+ rates = Rates.parse(data:)
51
+ dimensions = Dimensions.parse(data:)
48
52
 
49
53
  new(
50
54
  id: element.attr('data-unitid'),
@@ -3,9 +3,6 @@
3
3
  module PublicStorage
4
4
  # The rates (street + web) for a facility
5
5
  class Rates
6
- STREET_SELECTOR = '.unit-prices .unit-pricing .unit-strike-through-price'
7
- WEB_SELECTOR = '.unit-prices .unit-pricing .unit-price'
8
-
9
6
  # @attribute [rw] street
10
7
  # @return [Integer]
11
8
  attr_accessor :street
@@ -35,12 +32,12 @@ module PublicStorage
35
32
  "$#{@street} (street) | $#{@web} (web)"
36
33
  end
37
34
 
38
- # @param element [Nokogiri::XML::Element]
35
+ # @param data [Hash]
39
36
  #
40
37
  # @return [Rates]
41
- def self.parse(element:)
42
- street = Integer(element.at(STREET_SELECTOR).text.match(/(?<value>\d+)/)[:value])
43
- web = Integer(element.at(WEB_SELECTOR).text.match(/(?<value>\d+)/)[:value])
38
+ def self.parse(data:)
39
+ street = data['listprice']
40
+ web = data['saleprice']
44
41
  new(street:, web:)
45
42
  end
46
43
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PublicStorage
4
- VERSION = '0.3.0'
4
+ VERSION = '1.1.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: publicstorage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-27 00:00:00.000000000 Z
11
+ date: 2025-01-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http
@@ -83,9 +83,11 @@ files:
83
83
  - lib/publicstorage/address.rb
84
84
  - lib/publicstorage/cli.rb
85
85
  - lib/publicstorage/config.rb
86
+ - lib/publicstorage/crawl.rb
86
87
  - lib/publicstorage/crawler.rb
87
88
  - lib/publicstorage/dimensions.rb
88
89
  - lib/publicstorage/facility.rb
90
+ - lib/publicstorage/fetch_error.rb
89
91
  - lib/publicstorage/geocode.rb
90
92
  - lib/publicstorage/link.rb
91
93
  - lib/publicstorage/price.rb
@@ -98,8 +100,9 @@ licenses:
98
100
  metadata:
99
101
  rubygems_mfa_required: 'true'
100
102
  homepage_uri: https://github.com/ksylvest/publicstorage
101
- source_code_uri: https://github.com/ksylvest/publicstorage
102
- changelog_uri: https://github.com/ksylvest/publicstorage
103
+ source_code_uri: https://github.com/ksylvest/publicstorage/tree/v1.1.0
104
+ changelog_uri: https://github.com/ksylvest/publicstorage/releases/tag/v1.1.0
105
+ documentation_uri: https://publicstorage.ksylvest.com/
103
106
  post_install_message:
104
107
  rdoc_options: []
105
108
  require_paths: