alt 1.0.1 → 2.0.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: 0e07bfd91aa9ad068c22ce47b3fbe15791e63b2ea99f2d43b4fcd2d8cb0b8851
4
- data.tar.gz: 889d6574ec7935e0ef9a2fd087550c734373ace1b406501df2780015117db80e
3
+ metadata.gz: ff6375beb17a5e6f0ff9244e5b87cc4906f717644a34228fa22e58e58137070d
4
+ data.tar.gz: 206eee4de38abd0d516a341d72830c973f6ddac5c0e0978370d21550e6c38acc
5
5
  SHA512:
6
- metadata.gz: 3fb473e309e1cf32f142e21389d81e10d093d764a02ce8f80d965e9b30a3175509b00f7031c16dcab8925eac398246dfbab643f3e66e0c23442eefd7b6c51763
7
- data.tar.gz: 07a0aff38604788da60de4724f0864190d7c58bad7f4b8747bdbf94798a8ce412ac3f9de6cb1139d0d1334796ec2f0d09a0bcd2d44430e9d7a0e3872822a7e27
6
+ metadata.gz: 504da64a13fd24f9775f39533417c7875cb045f6c14e933a811a6e8dc8bbf5664a40b37901855bb7babc2766fdc8983f38b587de6c465a869fccd6f45d83b1ed
7
+ data.tar.gz: 07b2559f1ff20a650363c53980bb3588f581be639ab0e91f0d18903187df6c060c3c79e329fd78fc5f8430c15b17dcbb6f32984d1eae164c25e6bbdfa31df3bc
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## [Unreleased]
2
+
3
+ ## [2.0.0] - 2026-09-10
4
+
5
+ - [Breaking change] `Alt::Property#property_details` is gone, and `#facts` answers in its
6
+ place. Where the old method handed back Realtor's whole 73-key document, the new one
7
+ answers only the facts worth keeping, each named by what it is: `Bedrooms`, `Lot size`
8
+ (in acres), `Living area`, `Bathrooms`, `Year built`, `Stories`, `Garage spaces`,
9
+ `Fireplaces`, `Property type`, `Heating system`, `Heating fuel`, `Cooling`, `Roof`,
10
+ `Construction`, `Sewer`, `Foundation`, `Pool`, `Outdoor living`, `Fence` and
11
+ `Sprinklers`. A fact the listing does not state is left out rather than answered as a
12
+ zero or a `false`, since the response cannot tell "no pool" from "no mention of a pool".
13
+ - [Feature] The MLS dialect a listing is written in is mapped onto a closed set of options.
14
+ One fact is spelled many ways — `Warm Air`, `Forced Air` and `ForcedAir` are one heating
15
+ system, `Asphalt Shingle`, `Composition` and `Architectural Shingles` are one roof — and
16
+ a comma-joined value carries two facts at once, so `Forced Air, Natural Gas` answers a
17
+ heating system and a heating fuel separately. Every option was derived from 329 real
18
+ enrichments, and everything the mapping does not name answers `Other`.
19
+ - [Feature] `Coordinate` answers the point Realtor places the home at, as `[lat, lon]`. It
20
+ is the one entry not meant for storage: a host pins the place with it and drops it, since
21
+ a precise coordinate kept beside a street is personal data.
22
+ - [Fix] An acre is 43,560 square feet, not 43,561. `Lot size` was previously computed by
23
+ whoever called this gem, using the second number.
24
+
1
25
  ## [1.0.1] - 2026-08-24
2
26
 
3
27
  - [Fix] Raise Error when autocomplete doesn't return a valid address.
data/README.md CHANGED
@@ -1,8 +1,27 @@
1
1
  # Realtor API Ruby client
2
2
 
3
- Fetches property details from Realtor, reached through
3
+ Fetches the facts Realtor states about a home, reached through
4
4
  [RapidAPI](https://rapidapi.com/apidojo/api/realty-in-us).
5
5
 
6
+ - Source: https://github.com/claudiob/alt
7
+ - API reference: https://rubydoc.info/gems/alt
8
+
9
+ ## How to install
10
+
11
+ Install the gem system-wide:
12
+
13
+ ```sh
14
+ gem install alt
15
+ ```
16
+
17
+ Or add it to a Gemfile, pinned to the current major:
18
+
19
+ ```ruby
20
+ gem 'alt', '~> 2.0'
21
+ ```
22
+
23
+ `~> major.minor` means `bundle update` never crosses a breaking change.
24
+
6
25
  ## Authentication
7
26
 
8
27
  Subscribe to the Realtor API on RapidAPI and set your key as an environment variable:
@@ -16,11 +35,42 @@ The host (`realty-in-us.p.rapidapi.com`) is sent automatically as `X-RapidAPI-Ho
16
35
  ```ruby
17
36
  property = Alt::Property.new address: "4352 Desert Park Ave", unit: nil, city: "North Las Vegas", zipcode: "89085"
18
37
 
19
- property.property_details # => the parsed `/properties/v3/detail` response body
38
+ property.facts
39
+ # => { "Lot size" => 0.1837, "Living area" => 3000, "Bedrooms" => 5, "Bathrooms" => 2,
40
+ # "Year built" => 1999, "Stories" => 2, "Garage spaces" => 3, "Fireplaces" => 2,
41
+ # "Property type" => "Single family", "Heating system" => "Forced air",
42
+ # "Heating fuel" => "Natural gas", "Cooling" => "Central air",
43
+ # "Roof" => "Asphalt shingle", "Construction" => "Brick", "Sewer" => "Public",
44
+ # "Foundation" => "Slab", "Pool" => "Yes", "Outdoor living" => "Yes",
45
+ # "Fence" => "Yes", "Sprinklers" => "Yes", "Coordinate" => [36.2814, -115.1543] }
20
46
  ```
21
47
 
22
- Unlike HouseCanary, which returns separate `subject_address`, `public_records` and
23
- `assessment` objects, Realtor returns a single object, exposed here as `property_details`.
48
+ Every key is named after the fact it states, so a host stores each under whatever it calls
49
+ it. A fact the listing does not state is **left out** rather than answered as a zero or a
50
+ `false`: the response cannot tell "no pool" from "no mention of a pool", and a value that
51
+ says the second while meaning the first is worse than no value at all.
52
+
53
+ `Coordinate` is the one entry not meant for storage. It answers `[lat, lon]` so that a host
54
+ can place the home on a map; a precise coordinate kept beside a street address is personal
55
+ data, so pin with it and drop it.
56
+
57
+ Nothing else Realtor sends comes back. The response also carries the street, an `href`
58
+ whose slug is the street address, the listing agent's name, email and phones, and a Street
59
+ View URL embedding both the address and a live Maps key — an allow-list rather than a
60
+ deny-list is what keeps all of it out.
61
+
62
+ ## How a fact is read
63
+
64
+ Realtor answers two blocks worth reading: `description`, which states the home's own
65
+ figures and its kind, and `details`, an array of `{ category, text[] }` where each `text`
66
+ entry is a `"Label: value"` sentence written in whichever dialect the listing MLS uses.
67
+
68
+ One fact is spelled many ways, and a comma-joined value carries two facts at once. So each
69
+ choice is mapped onto a closed set of options — `Warm Air`, `Forced Air` and `ForcedAir`
70
+ are all `Forced air`; `Forced Air, Natural Gas` answers a heating system *and* a heating
71
+ fuel — and anything the mapping does not name answers `Other`. The options were derived
72
+ from 329 real enrichments rather than from the API's documentation, which does not describe
73
+ these fields.
24
74
 
25
75
  ## How a lookup works
26
76
 
@@ -30,13 +80,3 @@ Realtor has no address-to-detail endpoint, so a lookup takes two requests:
30
80
  2. `GET /properties/v3/detail?property_id=<id>` — returns the details
31
81
 
32
82
  `Alt::Error` is raised when either request fails, or when the address matches no property.
33
-
34
- ## Status
35
-
36
- The two endpoint paths above are confirmed to exist. The following identifiers are **not yet
37
- verified against a live response** (the account's RapidAPI quota was exhausted at the time of
38
- writing) and should be checked against the RapidAPI playground before relying on this client:
39
-
40
- - the `input` query parameter name for `/locations/v2/auto-complete`
41
- - the `autocomplete` array and its `mpr_id` field in that response
42
- - the `property_id` query parameter name for `/properties/v3/detail`
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alt
4
+ class Property
5
+ # Reads how a home is heated and cooled, which one line states in two parts.
6
+ module Climate
7
+ # The line an MLS states heating on, e.g. 'Heating Features: Forced Air, Natural Gas'.
8
+ HEATING = /\AHeating( Features| System)?\s*:/i
9
+
10
+ # The line it states cooling on, e.g. 'Cooling: Central Air'.
11
+ COOLING = /\ACooling( Features)?\s*:/i
12
+
13
+ private
14
+
15
+ def climate
16
+ heating = detail HEATING
17
+ { 'Heating system' => heating_system_of(heating),
18
+ 'Heating fuel' => heating_fuel_of(heating),
19
+ 'Cooling' => cooling_of(detail(COOLING)), }.compact
20
+ end
21
+
22
+ def heating_system_of(heating)
23
+ return unless heating
24
+
25
+ case heating
26
+ when /heat ?pump/i then 'Heat pump'
27
+ when /(warm|forced|hot) air|forcedair/i then 'Forced air'
28
+ when /hot water|forced water|radiator|steam|boiler/i then 'Hot water'
29
+ when /baseboard/i then 'Baseboard'
30
+ when /central/i then 'Central'
31
+ else 'Other'
32
+ end
33
+ end
34
+
35
+ def heating_fuel_of(heating)
36
+ case heating
37
+ when /electric/i then 'Electric'
38
+ when /propane/i then 'Propane'
39
+ when /natural gas|\bgas\b/i then 'Natural gas'
40
+ when /\boil\b/i then 'Oil'
41
+ end
42
+ end
43
+
44
+ def cooling_of(cooling)
45
+ return unless cooling
46
+
47
+ case cooling
48
+ when /central|package/i then 'Central air'
49
+ when /heat ?pump/i then 'Heat pump'
50
+ when /wall|window|room|individual/i then 'Wall or window'
51
+ when /evaporative/i then 'Evaporative'
52
+ when /fan/i then 'Ceiling fan'
53
+ else 'Other'
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alt
4
+ class Property
5
+ # Reads Realtor's `description`, which states a home's own figures and its kind.
6
+ module Described
7
+ # The square feet in an acre, which is the unit a lot is worth storing in.
8
+ ACRE = 43_560.0
9
+
10
+ private
11
+
12
+ def described
13
+ { 'Lot size' => acres, 'Living area' => description[:sqft],
14
+ 'Bedrooms' => description[:beds], 'Bathrooms' => description[:baths],
15
+ 'Year built' => description[:year_built], 'Stories' => description[:stories],
16
+ 'Garage spaces' => description[:garage],
17
+ 'Property type' => property_type_of(description[:type]), }.compact
18
+ end
19
+
20
+ def acres
21
+ sqft = description[:lot_sqft]
22
+ sqft / ACRE if sqft
23
+ end
24
+
25
+ def description = home[:description] || {}
26
+
27
+ def property_type_of(type)
28
+ return unless type
29
+
30
+ case type
31
+ when 'single_family' then 'Single family'
32
+ when /townhome/ then 'Townhome'
33
+ when 'condo', 'condos', 'apartment' then 'Condo'
34
+ when 'land', 'farm' then 'Land'
35
+ else 'Other'
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alt
4
+ class Property
5
+ # Reads Realtor's `details`, whose lines are sentences labeled by what they state.
6
+ module Detailed
7
+ private
8
+
9
+ def detail(label)
10
+ line = detail_lines.grep(label).first
11
+ line.split(':', 2).last.strip if line
12
+ end
13
+
14
+ def detail_lines
15
+ Array(home[:details]).flat_map { |one| Array one[:text] }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alt
4
+ class Property
5
+ # Reads what a home is roofed and clad with.
6
+ module Exterior
7
+ # The line an MLS states a roof on, e.g. 'Roof: Asphalt Shingle'.
8
+ ROOF = /\ARoof(ing)?\s*:/i
9
+
10
+ # The three headings it files the walls under, e.g. 'Construction Materials: Brick'.
11
+ CONSTRUCTION = /\A(Construction Materials|Building Exterior Type|Exterior)\s*:/i
12
+
13
+ private
14
+
15
+ def exterior
16
+ { 'Roof' => roof_of(detail(ROOF)),
17
+ 'Construction' => construction_of(detail(CONSTRUCTION)), }.compact
18
+ end
19
+
20
+ def roof_of(roof)
21
+ return unless roof
22
+
23
+ case roof
24
+ when /tile/i then 'Tile'
25
+ when /slate|slag/i then 'Slate'
26
+ when /metal|corrugated/i then 'Metal'
27
+ when /wood|shake|cedar/i then 'Wood'
28
+ when /shingle|asphalt|composition|fiberglass/i then 'Asphalt shingle'
29
+ else 'Other'
30
+ end
31
+ end
32
+
33
+ def construction_of(construction)
34
+ return unless construction
35
+
36
+ case construction
37
+ when /brick/i then 'Brick'
38
+ when /vinyl/i then 'Vinyl'
39
+ when /wood|cedar|clapboard|shake|frame/i then 'Wood'
40
+ when /aluminum/i then 'Aluminum'
41
+ when /stucco|plaster|stone|concrete|block|cement|hardi/i then 'Masonry'
42
+ else 'Other'
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alt
4
+ class Property
5
+ # Reads what a home has outside its walls.
6
+ module Grounds
7
+ # A pool of the home's own, anchored so that a community one is not taken for one.
8
+ POOL = /\APool/i
9
+
10
+ # Somewhere to sit out, e.g. 'Patio And Porch Features: Covered, Deck'.
11
+ OUTDOOR = /\A(Patio|Deck|Porch)/i
12
+
13
+ # A fence, however the MLS spells it: 'Fencing', 'Fence YN', 'Fenced Yard'.
14
+ FENCE = /\AFenc/i
15
+
16
+ # An irrigation system, e.g. 'Sprinkler System'. A fire sprinkler is labeled otherwise.
17
+ SPRINKLERS = /\ASprinkler/i
18
+
19
+ # The four headings an MLS states where the waste goes under, e.g. 'Sewer: Public'.
20
+ SEWER = /\A(Sewer|Septic|City Sewer|Public Sewer)/i
21
+
22
+ private
23
+
24
+ def grounds
25
+ { 'Pool' => ('Yes' if detail(POOL)),
26
+ 'Outdoor living' => ('Yes' if detail(OUTDOOR)),
27
+ 'Fence' => ('Yes' if detail(FENCE)),
28
+ 'Sprinklers' => ('Yes' if detail(SPRINKLERS)),
29
+ 'Sewer' => sewer_of(detail(SEWER)), }.compact
30
+ end
31
+
32
+ def sewer_of(sewer)
33
+ return unless sewer
34
+
35
+ case sewer
36
+ when /septic/i then 'Septic'
37
+ when /private/i then 'Private'
38
+ when /public|city|municipal|connected/i then 'Public'
39
+ else 'Other'
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alt
4
+ class Property
5
+ # Reads where Realtor says the home stands, which a host pins with rather than keeps.
6
+ module Placed
7
+ private
8
+
9
+ def placed
10
+ { 'Coordinate' => coordinate.values_at(:lat, :lon) }
11
+ end
12
+
13
+ def coordinate = home.dig(:location, :address, :coordinate) || {}
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alt
4
+ class Property
5
+ # Reads what a home stands on and what is built into it.
6
+ module Structure
7
+ # The line an MLS states what the house stands on, e.g. 'Foundation Details: Slab'.
8
+ FOUNDATION = /\AFoundation/i
9
+
10
+ # A counted fireplace, since 'Fireplace features' describes them rather than counting.
11
+ FIREPLACES = /\A(Number of )?Fireplaces\b[^:]*:\s*\d/i
12
+
13
+ private
14
+
15
+ def structure
16
+ { 'Foundation' => foundation_of(detail(FOUNDATION)),
17
+ 'Fireplaces' => detail(FIREPLACES)&.to_i, }.compact
18
+ end
19
+
20
+ def foundation_of(foundation)
21
+ return unless foundation
22
+
23
+ case foundation
24
+ when /slab/i then 'Slab'
25
+ when /crawl/i then 'Crawl space'
26
+ when /block/i then 'Block'
27
+ when /concrete|perimeter|poured/i then 'Concrete'
28
+ when /stone|brick/i then 'Stone'
29
+ else 'Other'
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/alt/property.rb CHANGED
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # A wrapper for the Realtor API, reached through RapidAPI, to fetch property details.
2
4
  # @see https://rapidapi.com/apidojo/api/realty-in-us
3
5
  module Alt
4
6
  # Embeds the logic of a Realtor property.
5
7
  class Property
8
+ include Climate, Described, Detailed, Exterior, Grounds, Placed, Structure
9
+
6
10
  # The RapidAPI host serving the Realtor data.
7
11
  HOST = 'realty-in-us.p.rapidapi.com'
8
12
 
@@ -15,13 +19,15 @@ module Alt
15
19
  @data = data
16
20
  end
17
21
 
18
- # @return [Hash] Details about the property
19
- def property_details = details
22
+ # @return [Hash] every fact Realtor states about the property, named by what it is.
23
+ def facts = [climate, described, exterior, grounds, placed, structure].inject :merge
20
24
 
21
25
  private
22
26
 
23
- def details
24
- @details ||= fetch
27
+ def home = response.dig(:data, :home) || {}
28
+
29
+ def response
30
+ @response ||= fetch
25
31
  end
26
32
 
27
33
  # Realtor exposes no address-to-detail endpoint, so the address is resolved to an id first.
data/lib/alt/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Alt
2
- VERSION = '1.0.1'
2
+ VERSION = '2.0.0'
3
3
  end
data/lib/alt.rb CHANGED
@@ -6,4 +6,11 @@ require 'net/http'
6
6
  require 'uri'
7
7
 
8
8
  require 'alt/error'
9
+ require 'alt/property/climate'
10
+ require 'alt/property/described'
11
+ require 'alt/property/detailed'
12
+ require 'alt/property/exterior'
13
+ require 'alt/property/grounds'
14
+ require 'alt/property/placed'
15
+ require 'alt/property/structure'
9
16
  require 'alt/property'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo
@@ -36,6 +36,13 @@ files:
36
36
  - lib/alt.rb
37
37
  - lib/alt/error.rb
38
38
  - lib/alt/property.rb
39
+ - lib/alt/property/climate.rb
40
+ - lib/alt/property/described.rb
41
+ - lib/alt/property/detailed.rb
42
+ - lib/alt/property/exterior.rb
43
+ - lib/alt/property/grounds.rb
44
+ - lib/alt/property/placed.rb
45
+ - lib/alt/property/structure.rb
39
46
  - lib/alt/version.rb
40
47
  homepage: https://github.com/claudiob/alt
41
48
  licenses:
@@ -43,7 +50,8 @@ licenses:
43
50
  metadata:
44
51
  homepage_uri: https://github.com/claudiob/alt
45
52
  source_code_uri: https://github.com/claudiob/alt
46
- changelog_uri: https://github.com/claudiob/alt
53
+ changelog_uri: https://github.com/claudiob/alt/blob/main/CHANGELOG.md
54
+ documentation_uri: https://rubydoc.info/gems/alt
47
55
  rdoc_options: []
48
56
  require_paths:
49
57
  - lib