publicstorage 0.3.0 → 1.0.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: 385e04642b7995999e422333ae7c251c4e3556ae9958b0df0f35a387ca35297a
4
+ data.tar.gz: 0c99f850bedd80f482187364625aa583518768ad6fc7f4b749f5dafdfe0871d8
5
5
  SHA512:
6
- metadata.gz: b1bed2e588707393e89bf67a416bc8f3622dda29e3c8b4754cfd015f224555530a628cce7cbb68e5c4787c745905493ae8c6e6efe14a8c6d84fae852f213474b
7
- data.tar.gz: e46336139bbf2f3054cff093e650b88c5e84b7bb0beaa876b5f833d192c641fa3e48449ca27a3af1215a2fdd9dafd284c937348951248e5704ca62668b43f11d
6
+ metadata.gz: 25af8483c84aded81acac087be7e7bf7c42a404200d8d1091b7f32184de6a61fd6abccdff9cd4d6b2efcf582f3619c37250adeb21440232d3afa90c685ea2894
7
+ data.tar.gz: d623ad587cb3c5b3d33aad2221002a411211f1cc5d39741b86e1ffa9f64732bc1ba9220a72e7e05bec40e852589a8ce09097a89ac22db489bda30df7776d3322
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]
@@ -41,11 +41,11 @@ module PublicStorage
41
41
  "#{format('%g', @width)}' × #{format('%g', @depth)}' (#{@sqft} sqft)"
42
42
  end
43
43
 
44
- # @param element [Nokogiri::XML::Element]
44
+ # @param data [Hash]
45
45
  #
46
46
  # @return [Dimensions]
47
- def self.parse(element:)
48
- match = element.at(SELECTOR).text.match(/(?<depth>[\d\.]+)'x(?<width>[\d\.]+)'/)
47
+ def self.parse(data:)
48
+ match = data['dimension'].match(/(?<depth>[\d\.]+)'x(?<width>[\d\.]+)'/)
49
49
  depth = Float(match[:depth])
50
50
  width = Float(match[:width])
51
51
  sqft = Integer(depth * width)
@@ -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.0.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.0.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: 2024-12-18 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.0.0
104
+ changelog_uri: https://github.com/ksylvest/publicstorage/releases/tag/v1.0.0
105
+ documentation_uri: https://publicstorage.ksylvest.com/
103
106
  post_install_message:
104
107
  rdoc_options: []
105
108
  require_paths: