hub_spot 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +64 -0
- data/.travis.yml +18 -0
- data/Gemfile +5 -1
- data/README.md +17 -36
- data/Rakefile +4 -2
- data/bin/console +1 -0
- data/coverage +1 -0
- data/hub_spot.gemspec +5 -3
- data/lib/hub_spot/configuration.rb +3 -1
- data/lib/hub_spot/contact.rb +11 -0
- data/lib/hub_spot/http.rb +12 -6
- data/lib/hub_spot/http_api_calls/base.rb +19 -0
- data/lib/hub_spot/http_api_calls/contact/create_or_update.rb +50 -0
- data/lib/hub_spot/http_api_calls/error.rb +8 -0
- data/lib/hub_spot/oauth/client.rb +14 -16
- data/lib/hub_spot/oauth/token.rb +2 -0
- data/lib/hub_spot/oauth/token_store.rb +28 -0
- data/lib/hub_spot/oauth.rb +3 -1
- data/lib/hub_spot/version.rb +3 -1
- data/lib/hub_spot.rb +7 -0
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7280d6ab4353911e48c3ac650bcf38b7ad450763
|
4
|
+
data.tar.gz: 38a4283f116eb6da41c24ec2d5928dcb236d7922
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '09315506c63ff8d02cd1855d9e3015546e1ac3b632541c828b9a3f8789dac67a06169fdc4a156c4236051194230869517d6bfe3ea86ea227f45bba1921e01cac'
|
7
|
+
data.tar.gz: 85e4036342659616ac94ee36c98418fd4a3b7cd9a09358b5a4ae376a0d87811405108b386d0024cbb81153ac9cba6f74c8753b021819780a01d04773f779c778
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# For possible values, see: https://github.com/bbatsov/rubocop/blob/master/config/default.yml
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
TargetRubyVersion: 2.4
|
5
|
+
DisplayStyleGuide: false
|
6
|
+
# Exclude:
|
7
|
+
# - db/**/*
|
8
|
+
# StyleGuideCopsOnly: true
|
9
|
+
|
10
|
+
Rails:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Style/TrailingCommaInLiteral:
|
14
|
+
EnforcedStyleForMultiline: comma
|
15
|
+
|
16
|
+
Style/TrailingCommaInArguments:
|
17
|
+
EnforcedStyleForMultiline: comma
|
18
|
+
|
19
|
+
Style/AndOr:
|
20
|
+
# Whether `and` and `or` are banned only in conditionals (conditionals)
|
21
|
+
# or completely (always).
|
22
|
+
EnforcedStyle: conditionals
|
23
|
+
|
24
|
+
# Style/DoubleNegation:
|
25
|
+
# Enabled: false
|
26
|
+
|
27
|
+
# Cop supports --auto-correct.
|
28
|
+
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
29
|
+
Style/StringLiterals:
|
30
|
+
EnforcedStyle: double_quotes
|
31
|
+
|
32
|
+
# Style/NumericLiterals:
|
33
|
+
# Enabled: false
|
34
|
+
|
35
|
+
Style/TrailingUnderscoreVariable:
|
36
|
+
AllowNamedUnderscoreVariables: true
|
37
|
+
|
38
|
+
# Style/GuardClause:
|
39
|
+
# Enabled: false
|
40
|
+
|
41
|
+
# Lint/HandleExceptions:
|
42
|
+
# Exclude:
|
43
|
+
# - 'bin/rails'
|
44
|
+
# - 'bin/rake'
|
45
|
+
|
46
|
+
# Style/MultilineBlockChain:
|
47
|
+
# Enabled: false
|
48
|
+
|
49
|
+
Metrics/LineLength:
|
50
|
+
Max: 110
|
51
|
+
# Exclude:
|
52
|
+
# - 'bin/spring'
|
53
|
+
|
54
|
+
Layout/MultilineMethodCallIndentation:
|
55
|
+
EnforcedStyle: indented_relative_to_receiver
|
56
|
+
|
57
|
+
Style/Documentation:
|
58
|
+
Enabled: false
|
59
|
+
|
60
|
+
Metrics/BlockLength:
|
61
|
+
Exclude:
|
62
|
+
- "spec/**/*_spec.rb"
|
63
|
+
# - "spec/*_spec.rb"
|
64
|
+
- "hub_spot.gemspec"
|
data/.travis.yml
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
sudo: false
|
2
|
+
env:
|
3
|
+
global:
|
4
|
+
- GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi)
|
2
5
|
language: ruby
|
3
6
|
rvm:
|
4
7
|
- 2.4.1
|
5
8
|
before_install: gem install bundler -v 1.15.3
|
9
|
+
before_script:
|
10
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
11
|
+
- chmod +x ./cc-test-reporter
|
12
|
+
script:
|
13
|
+
- bundle exec rspec
|
14
|
+
# Preferably you will run test-reporter on branch update events. But
|
15
|
+
# if you setup travis to build PR updates only, you don't need to run
|
16
|
+
# the line below
|
17
|
+
- if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi
|
18
|
+
# In the case where travis is setup to build PR updates only,
|
19
|
+
# uncomment the line below
|
20
|
+
# - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
21
|
+
notifications:
|
22
|
+
email:
|
23
|
+
- djr@DanielRabinowitz.com
|
data/Gemfile
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
source "https://rubygems.org"
|
2
4
|
|
3
|
-
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
4
6
|
|
5
7
|
# Specify your gem's dependencies in hub_spot.gemspec
|
6
8
|
gemspec
|
9
|
+
|
10
|
+
gem "codeclimate-test-reporter", group: :test, require: nil
|
data/README.md
CHANGED
@@ -1,51 +1,32 @@
|
|
1
1
|
# HubSpot
|
2
2
|
|
3
|
-
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/hub_spot.svg)](https://badge.fury.io/rb/hub_spot)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/danrabinowitz/hub_spot/badges/gpa.svg)](https://codeclimate.com/github/danrabinowitz/hub_spot)
|
5
|
+
[![Build Status](https://travis-ci.org/danrabinowitz/hub_spot.svg?branch=master)](https://travis-ci.org/danrabinowitz/hub_spot)
|
6
|
+
[![Test Coverage](https://codeclimate.com/github/danrabinowitz/hub_spot/badges/coverage.svg)](https://codeclimate.com/github/danrabinowitz/hub_spot/coverage)
|
7
|
+
[![Gem](https://img.shields.io/gem/dt/hub_spot.svg?maxAge=2592000)](https://rubygems.org/gems/hub_spot)
|
4
8
|
|
5
|
-
1. Add rubocop
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
HubSpot::Configuration.cache = Moneta.new(:LRUHash, expires: true)
|
11
|
-
HubSpot::Oauth.access_token
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hub_spot`. To experiment with that code, run `bin/console` for an interactive prompt.
|
17
|
-
|
18
|
-
TODO: Delete this and the text above, and describe your gem
|
19
|
-
|
20
|
-
## Installation
|
21
|
-
|
22
|
-
Add this line to your application's Gemfile:
|
10
|
+
## Usage
|
23
11
|
|
12
|
+
1) Add the gem to your Gemfile
|
24
13
|
```ruby
|
25
14
|
gem 'hub_spot'
|
26
15
|
```
|
27
16
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
## Usage
|
37
|
-
|
38
|
-
TODO: Write usage instructions here
|
39
|
-
|
40
|
-
## Development
|
17
|
+
2) Run the following setup code on initialization:
|
18
|
+
```ruby
|
19
|
+
HubSpot::Configuration.client_id = ENV.fetch("HUBSPOT_CLIENT_ID")
|
20
|
+
HubSpot::Configuration.client_secret = ENV.fetch("HUBSPOT_CLIENT_SECRET")
|
21
|
+
HubSpot::Configuration.refresh_token = ENV.fetch("HUBSPOT_REFRESH_TOKEN")
|
22
|
+
HubSpot::Configuration.redirect_uri = ENV.fetch("HUBSPOT_REDIRECT_URI")
|
23
|
+
```
|
41
24
|
|
42
|
-
|
25
|
+
3) Call the api...
|
43
26
|
|
44
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
45
27
|
|
46
28
|
## Contributing
|
47
|
-
|
48
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hub_spot. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
29
|
+
PRs are welcome!
|
49
30
|
|
50
31
|
## License
|
51
32
|
|
@@ -53,4 +34,4 @@ The gem is available as open source under the terms of the [MIT License](http://
|
|
53
34
|
|
54
35
|
## Code of Conduct
|
55
36
|
|
56
|
-
Everyone interacting in the HubSpot project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
37
|
+
Everyone interacting in the HubSpot project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/danrabinowitz/hub_spot/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "bundler/gem_tasks"
|
2
4
|
require "rspec/core/rake_task"
|
3
5
|
|
4
6
|
RSpec::Core::RakeTask.new(:spec)
|
5
7
|
|
6
|
-
require
|
8
|
+
require "rubocop/rake_task"
|
7
9
|
RuboCop::RakeTask.new
|
8
10
|
|
9
|
-
task :
|
11
|
+
task default: %i[spec rubocop]
|
data/bin/console
CHANGED
data/coverage
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
./tmp/coverage
|
data/hub_spot.gemspec
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
2
4
|
lib = File.expand_path("../lib", __FILE__)
|
3
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
6
|
require "hub_spot/version"
|
@@ -9,8 +11,8 @@ Gem::Specification.new do |spec|
|
|
9
11
|
spec.authors = ["Dan Rabinowitz"]
|
10
12
|
spec.email = ["djr@DanielRabinowitz.com"]
|
11
13
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
14
|
+
spec.summary = "Omakase ruby API for HubSpot"
|
15
|
+
spec.description = "Omakase ruby API for HubSpot"
|
14
16
|
spec.homepage = "https://github.com/danrabinowitz/hub_spot"
|
15
17
|
spec.license = "MIT"
|
16
18
|
|
@@ -23,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
23
25
|
"public gem pushes."
|
24
26
|
end
|
25
27
|
|
26
|
-
spec.files
|
28
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
29
|
f.match(%r{^(test|spec|features)/})
|
28
30
|
end
|
29
31
|
spec.bindir = "exe"
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module HubSpot
|
2
4
|
class Configuration
|
3
|
-
ATTRIBUTES = %i
|
5
|
+
ATTRIBUTES = %i[api_host client_id client_secret redirect_uri refresh_token logger].freeze
|
4
6
|
|
5
7
|
class << self
|
6
8
|
attr_accessor(*ATTRIBUTES)
|
data/lib/hub_spot/http.rb
CHANGED
@@ -1,21 +1,27 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
2
4
|
|
3
5
|
module HubSpot
|
4
6
|
module HTTP
|
5
7
|
DEFAULT_HEADERS = {
|
6
|
-
:
|
7
|
-
:
|
8
|
-
}
|
8
|
+
accept: "application/json",
|
9
|
+
content_type: "application/x-www-form-urlencoded;charset=utf-8",
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
# Authentication credentials for HTTP authentication.
|
13
|
+
AUTHORIZATION = "Authorization"
|
9
14
|
|
10
15
|
module_function
|
16
|
+
|
11
17
|
def post(url:, post_body: nil, headers: DEFAULT_HEADERS)
|
12
18
|
uri = URI(url)
|
13
|
-
Net::HTTP.start(uri.host, uri.port, :
|
19
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
14
20
|
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
15
21
|
request.body = post_body.to_json unless post_body.nil?
|
16
22
|
|
17
23
|
# Send the request
|
18
|
-
|
24
|
+
http.request(request)
|
19
25
|
end
|
20
26
|
end
|
21
27
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HubSpot
|
4
|
+
module HTTPApiCalls
|
5
|
+
module Base
|
6
|
+
def response
|
7
|
+
JSON.parse(raw_response.body)
|
8
|
+
end
|
9
|
+
|
10
|
+
def raw_response
|
11
|
+
make_the_call
|
12
|
+
end
|
13
|
+
|
14
|
+
def default_headers
|
15
|
+
HubSpot::HTTP::DEFAULT_HEADERS
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HubSpot
|
4
|
+
module HTTPApiCalls
|
5
|
+
module Contact
|
6
|
+
class CreateOrUpdate
|
7
|
+
include ::HubSpot::HTTPApiCalls::Base
|
8
|
+
URL = "https://api.hubapi.com/contacts/v1/contact/createOrUpdate/email/%<email>s"
|
9
|
+
|
10
|
+
def initialize(properties)
|
11
|
+
@properties = properties
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_reader :properties
|
17
|
+
|
18
|
+
def make_the_call
|
19
|
+
response = HubSpot::HTTP.post(url: url, headers: headers, post_body: post_body)
|
20
|
+
puts response
|
21
|
+
response
|
22
|
+
end
|
23
|
+
|
24
|
+
def url
|
25
|
+
format(URL, email: email)
|
26
|
+
end
|
27
|
+
|
28
|
+
def email
|
29
|
+
properties.fetch("email")
|
30
|
+
end
|
31
|
+
|
32
|
+
def headers
|
33
|
+
default_headers.merge(HubSpot::HTTP::AUTHORIZATION => auth_header_value)
|
34
|
+
end
|
35
|
+
|
36
|
+
def auth_header_value
|
37
|
+
"Bearer #{HubSpot::OAuth.access_token}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def post_body
|
41
|
+
{ "properties" => properties_array }
|
42
|
+
end
|
43
|
+
|
44
|
+
def properties_array
|
45
|
+
properties.map { |k, v| { "property" => k.to_s, "value" => v } }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -1,32 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module HubSpot
|
2
4
|
module OAuth
|
3
5
|
module Client
|
4
|
-
URL = "https://api.hubapi.com/oauth/v1/token?grant_type=refresh_token&client_id
|
5
|
-
@token = Token.new(expires_at: Time.now - 1)
|
6
|
-
|
7
|
-
module_function
|
6
|
+
URL = "https://api.hubapi.com/oauth/v1/token?grant_type=refresh_token&client_id=%<client_id>s&client_secret=%<client_secret>s&redirect_uri=%<redirect_uri>s&refresh_token=%<refresh_token>s" # rubocop:disable Metrics/LineLength
|
8
7
|
|
9
|
-
|
10
|
-
if @token.expired?
|
11
|
-
@token = Token.new(token_params)
|
12
|
-
else
|
13
|
-
@token
|
14
|
-
end
|
15
|
-
end
|
8
|
+
class APIError < StandardError; end
|
16
9
|
|
17
|
-
|
18
|
-
# private
|
10
|
+
module_function
|
19
11
|
|
20
12
|
def token_params
|
21
13
|
expires_in, token_value = api_response.values_at("expires_in", "access_token")
|
22
14
|
expires_at = Time.now + expires_in
|
23
|
-
{value: token_value, expires_at: expires_at}
|
15
|
+
{ value: token_value, expires_at: expires_at }
|
24
16
|
end
|
25
17
|
|
18
|
+
# Below here are private methods
|
26
19
|
def api_response
|
27
20
|
response = HubSpot::HTTP.post(url: url)
|
28
|
-
|
29
|
-
|
21
|
+
if response.code.to_s != "200"
|
22
|
+
raise APIError, "OAuth API call returned a #{response.code} != 200"
|
23
|
+
end
|
30
24
|
JSON.parse(response.body)
|
31
25
|
end
|
32
26
|
|
@@ -42,6 +36,10 @@ module HubSpot
|
|
42
36
|
refresh_token: HubSpot::Configuration.refresh_token,
|
43
37
|
}
|
44
38
|
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
private :api_response, :url, :url_params
|
42
|
+
end
|
45
43
|
end
|
46
44
|
end
|
47
45
|
end
|
data/lib/hub_spot/oauth/token.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HubSpot
|
4
|
+
module OAuth
|
5
|
+
module TokenStore
|
6
|
+
EXPIRED_TOKEN = Token.new(expires_at: Time.now - 1).freeze
|
7
|
+
@token = EXPIRED_TOKEN
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def value
|
12
|
+
token.value
|
13
|
+
end
|
14
|
+
|
15
|
+
def expire
|
16
|
+
@token = EXPIRED_TOKEN
|
17
|
+
end
|
18
|
+
|
19
|
+
def token
|
20
|
+
if @token.expired?
|
21
|
+
@token = Token.new(Client.token_params)
|
22
|
+
else
|
23
|
+
@token
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/hub_spot/oauth.rb
CHANGED
data/lib/hub_spot/version.rb
CHANGED
data/lib/hub_spot.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "hub_spot/configuration"
|
2
4
|
require "hub_spot/http"
|
5
|
+
require "hub_spot/http_api_calls/base"
|
6
|
+
require "hub_spot/http_api_calls/error"
|
7
|
+
require "hub_spot/http_api_calls/contact/create_or_update"
|
8
|
+
require "hub_spot/contact"
|
3
9
|
require "hub_spot/oauth"
|
4
10
|
require "hub_spot/oauth/token"
|
11
|
+
require "hub_spot/oauth/token_store"
|
5
12
|
require "hub_spot/oauth/client"
|
6
13
|
require "hub_spot/version"
|
7
14
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hub_spot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Rabinowitz
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-07-
|
11
|
+
date: 2017-07-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -159,6 +159,7 @@ extra_rdoc_files: []
|
|
159
159
|
files:
|
160
160
|
- ".gitignore"
|
161
161
|
- ".rspec"
|
162
|
+
- ".rubocop.yml"
|
162
163
|
- ".simplecov"
|
163
164
|
- ".travis.yml"
|
164
165
|
- CODE_OF_CONDUCT.md
|
@@ -168,13 +169,19 @@ files:
|
|
168
169
|
- Rakefile
|
169
170
|
- bin/console
|
170
171
|
- bin/setup
|
172
|
+
- coverage
|
171
173
|
- hub_spot.gemspec
|
172
174
|
- lib/hub_spot.rb
|
173
175
|
- lib/hub_spot/configuration.rb
|
176
|
+
- lib/hub_spot/contact.rb
|
174
177
|
- lib/hub_spot/http.rb
|
178
|
+
- lib/hub_spot/http_api_calls/base.rb
|
179
|
+
- lib/hub_spot/http_api_calls/contact/create_or_update.rb
|
180
|
+
- lib/hub_spot/http_api_calls/error.rb
|
175
181
|
- lib/hub_spot/oauth.rb
|
176
182
|
- lib/hub_spot/oauth/client.rb
|
177
183
|
- lib/hub_spot/oauth/token.rb
|
184
|
+
- lib/hub_spot/oauth/token_store.rb
|
178
185
|
- lib/hub_spot/version.rb
|
179
186
|
homepage: https://github.com/danrabinowitz/hub_spot
|
180
187
|
licenses:
|