publicstorage 0.3.0 → 1.1.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
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: