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 +4 -4
- data/README.md +8 -2
- data/lib/publicstorage/cli.rb +9 -3
- data/lib/publicstorage/config.rb +1 -1
- data/lib/publicstorage/crawl.rb +40 -0
- data/lib/publicstorage/crawler.rb +0 -9
- data/lib/publicstorage/dimensions.rb +25 -14
- data/lib/publicstorage/fetch_error.rb +12 -0
- data/lib/publicstorage/price.rb +6 -2
- data/lib/publicstorage/rates.rb +4 -7
- data/lib/publicstorage/version.rb +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f797ad8918b9cea8da35957e83b8bbf56ab02f1bfae95ee24a0fe1a316d1117d
|
4
|
+
data.tar.gz: e2a976a36b0fc0d5edefde19c7234b778257341abc9c9507029bac2d6e31f1e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb338e5697de874ba118f47828d19ed3ed817cd136aac67372064fcea2dabde722c34cb39924b55e91bbbca3802b8469b7caf6a5fef5fa64abf559a2c2e38090
|
7
|
+
data.tar.gz: 8ed743ff4a0a59deefe92064d62345d6deaaceb860e597583758b8e7d18eac6f0db6b12f67ef0324212882ef1fea14009501b2ab0566d139241ae4d78d60f7eb
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# Public Storage
|
2
2
|
|
3
3
|
[](https://github.com/ksylvest/publicstorage/blob/main/LICENSE)
|
4
4
|
[](https://rubygems.org/gems/publicstorage)
|
@@ -6,6 +6,8 @@
|
|
6
6
|
[](https://publicstorage.ksylvest.com)
|
7
7
|
[](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 =
|
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
|
+
```
|
data/lib/publicstorage/cli.rb
CHANGED
@@ -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
|
-
|
34
|
-
|
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
|
data/lib/publicstorage/config.rb
CHANGED
@@ -12,7 +12,7 @@ module PublicStorage
|
|
12
12
|
attr_accessor :timeout
|
13
13
|
|
14
14
|
def initialize
|
15
|
-
@user_agent = ENV.fetch('
|
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 +
|
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]
|
17
|
-
#
|
18
|
-
attr_accessor :
|
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
|
23
|
-
def initialize(depth:, width:,
|
24
|
+
# @param height [Float]
|
25
|
+
def initialize(depth:, width:, height: DEFAULT_HEIGHT)
|
24
26
|
@depth = depth
|
25
27
|
@width = width
|
26
|
-
@
|
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
|
-
"
|
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)}' (#{
|
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
|
56
|
+
# @param data [Hash]
|
45
57
|
#
|
46
58
|
# @return [Dimensions]
|
47
|
-
def self.parse(
|
48
|
-
match =
|
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:,
|
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
|
data/lib/publicstorage/price.rb
CHANGED
@@ -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
|
-
|
47
|
-
|
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'),
|
data/lib/publicstorage/rates.rb
CHANGED
@@ -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
|
35
|
+
# @param data [Hash]
|
39
36
|
#
|
40
37
|
# @return [Rates]
|
41
|
-
def self.parse(
|
42
|
-
street =
|
43
|
-
web =
|
38
|
+
def self.parse(data:)
|
39
|
+
street = data['listprice']
|
40
|
+
web = data['saleprice']
|
44
41
|
new(street:, web:)
|
45
42
|
end
|
46
43
|
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:
|
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:
|
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:
|