klimt 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cf88f548e7806cd204cdad0f709d4fcba34cf197
4
+ data.tar.gz: 462adaad08830d3752279b11790f99ddeaec983d
5
+ SHA512:
6
+ metadata.gz: 52962eeb267cf8c8247ccd64ee2d8a9c218437eae74b1ae470a77555b85199031bfe2d849add0b656c3c98b3d2c8017834bb5e042e8387f185fef144f64dda3f
7
+ data.tar.gz: dd4f66ba0bb5dd9e345178927c6c266f71efe8cdef85d069d3d58809ed45c6616903431bd4b4187999bd6b4f7b3d117db0727daae4a1e46e33b3cc9c1506cb1b
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+
14
+ .byebug_history
15
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop ADDED
@@ -0,0 +1 @@
1
+ --display-cop-names
data/.rubocop.yml ADDED
@@ -0,0 +1,23 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ AllCops:
4
+ Exclude:
5
+ - klimt.gemspec
6
+ - vendor/**/*
7
+
8
+ Metrics/LineLength:
9
+ Max: 132 # good for desktop Github
10
+
11
+ Metrics/BlockLength:
12
+ Exclude:
13
+ - spec/**/*
14
+
15
+
16
+ Style/FrozenStringLiteralComment:
17
+ Enabled: false
18
+
19
+ Style/Documentation:
20
+ Enabled: false
21
+
22
+ Style/FormatString:
23
+ EnforcedStyle: percent
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,29 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2017-02-10 10:19:12 -0500 using RuboCop version 0.47.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 2
10
+ Metrics/AbcSize:
11
+ Max: 35
12
+
13
+ # Offense count: 1
14
+ # Configuration parameters: CountComments.
15
+ Metrics/ClassLength:
16
+ Max: 110
17
+
18
+ # Offense count: 1
19
+ Metrics/CyclomaticComplexity:
20
+ Max: 10
21
+
22
+ # Offense count: 2
23
+ # Configuration parameters: CountComments.
24
+ Metrics/MethodLength:
25
+ Max: 33
26
+
27
+ # Offense count: 1
28
+ Metrics/PerceivedComplexity:
29
+ Max: 13
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ before_install: gem install bundler -v 1.14.3
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at anandaroop.roy+github@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in klimt.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Anandaroop Roy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Klimt
2
+ [![CircleCI](https://circleci.com/gh/anandaroop/klimt.svg?style=svg)](https://circleci.com/gh/anandaroop/klimt) [![Build Status](https://travis-ci.org/anandaroop/klimt.svg?branch=master)](https://travis-ci.org/anandaroop/klimt)
3
+
4
+ Klimt is a **C**ommand **L**ine **I**nterface **M**etadata **T**ool. That almost spells Klimt. So close.
5
+
6
+ It makes it dead simple to view Artsy API json responses from the command line.
7
+
8
+ ## Usage
9
+
10
+ Klimt has four basic subcommands which will work with most REST-ful endpoints in the API (see [caveat](#caveat-re-root-level-endpoints)).
11
+
12
+ ### View a resource
13
+
14
+ Supply the model name as it appears in the v1 API endpoint, and an id:
15
+
16
+ ```sh
17
+ $ klimt find partner gagosian-gallery
18
+ $ klimt find artist damien-hirst
19
+ ```
20
+
21
+ ### List a collection of resources
22
+
23
+ Supply the (usually plural) model name as it appears in the v1 API endpoint, and optionally some API parameters:
24
+
25
+ ```sh
26
+ $ klimt list partners
27
+ $ klimt list partners eligible_for_listing=true near=30,-90
28
+ ```
29
+
30
+ ### Find a resource via term search
31
+
32
+ Supply the term to search for (enclosed in quotes if it contains whitespace), and optionally a list of space-delimited [indexes](https://github.com/artsy/gravity/blob/baf6bd35f4c5c1a6011d0608d641e8d6608124e7/app/api/v1/match_endpoint.rb#L150) to constrain the search.
33
+
34
+ ```sh
35
+ $ klimt search "Gagosian Gallery"
36
+ $ klimt search Gagosian --indexes=Article Artwork
37
+ ```
38
+
39
+ ### Count a resource
40
+
41
+ Supply the (usually plural) model name as it appears in the v1 API endpoint, and optionally some API parameters:
42
+
43
+ ```sh
44
+ $ klimt count cultures
45
+ $ klimt count cultures nationalities=true
46
+ ```
47
+
48
+ ### Caveat re: root-level endpoints
49
+
50
+ The find, list and count commands work great when the API endpoint in question is structured as a root-level endpoint rather than a nested one. So for now… `/api/v1/partner/<id>` :heavy_check_mark: but `/api/v1/partner/<id>/locations` :x:.
51
+
52
+ See [the discussion](https://github.com/artsy/potential/blob/521d34796e2df87406cc0e780db1e44b1ac9884a/Playbook.md#out-with-the-old) under "Gravity's v1 API" in Potential.
53
+
54
+ ### Custom subcommands
55
+
56
+ If a resource can't be retrieved because it's not a root-level endoint, or if some further custom behavior is desired, it's easy enough to create a new subcommand to encapsulate these requirements, e.g. the [partner subcommand](https://github.com/anandaroop/klimt/blob/9ac2d3f341abe4d9f482d64b82a56e1248a8792b/lib/klimt/command.rb#L59-L60) or the [city subcommand](https://github.com/anandaroop/klimt/blob/9ac2d3f341abe4d9f482d64b82a56e1248a8792b/lib/klimt/command.rb#L64-L65).
57
+
58
+ For example, this partner subcommand results in several fetches and a detailed report:
59
+
60
+ ```sh
61
+ $ klimt partner check-locations gagosian-gallery -x=-90 -y=30
62
+ ```
63
+
64
+ ### Authentication
65
+
66
+ Klimt — like the Heroku CLI client — uses [Netrc](https://github.com/heroku/netrc) to manage credentials. The first time you use it it will ask for a login, generate a token and save the token in `~/.netrc`.
67
+
68
+ ## Klimt :sparkling_heart: JQ
69
+
70
+ [JQ](https://stedolan.github.io/jq/) is a command line JSON pretty-printing and transformation tool that works great with Klimt
71
+
72
+ ```sh
73
+ $ klimt list partners | jq '.[] | { id, name }'
74
+ $ klimt find partner moma-ps1 | jq '.name'
75
+ ```
76
+
77
+ In fact Klimt will detect if JQ is installed, and if so will sometimes use it for nicer output, such as if you pass Klimt the `--color` flag:
78
+
79
+ ```sh
80
+ $ klimt find artist andy-warhol --color
81
+ ```
82
+
83
+ ## Installation
84
+
85
+ Since it's not distributed via RubyGems, you'll fetch and build it yourself which is as simple as:
86
+
87
+ ```sh
88
+ $ git clone https://github.com/anandaroop/klimt.git
89
+ $ cd klimt
90
+ # Set up your gravity keys
91
+ $ gem build klimt.gemspec
92
+ $ gem install klimt*.gem
93
+ ```
94
+
95
+ Klimt uses a Gravity `ClientApplication`, whose id and secret you'll have to supply in your environment as `KLIMT_ID` and `KLIMT_SECRET`. (Available in 1Password)
96
+
97
+ ```sh
98
+ $ KLIMT_ID=<replace> KLIMT_SECRET=<replace> klimt help
99
+ ```
100
+
101
+ Or just add these env variables to your shell startup script.
102
+
103
+ If you'd like auto-completion in zsh, add `eval "$(klimt zsh-completion)"` to the end of your `.zshrc`.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ RuboCop::RakeTask.new(:rubocop) do |t|
8
+ t.options = ['--display-cop-names']
9
+ end
10
+
11
+ task default: [:rubocop, :spec]
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'klimt'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/circle.yml ADDED
@@ -0,0 +1,4 @@
1
+ test:
2
+ override:
3
+ - bundle exec rubocop
4
+ - bundle exec rspec
data/exe/klimt ADDED
@@ -0,0 +1,5 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'klimt'
4
+
5
+ Klimt::Command.start
data/klimt.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'klimt/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'klimt'
8
+ spec.version = Klimt::VERSION
9
+ spec.authors = ['Anandaroop Roy']
10
+ spec.email = ['anandaroop.roy+github@gmail.com']
11
+
12
+ spec.summary = 'CLI for the Artsy API'
13
+ spec.homepage = 'https://github.com/anandaroop/klimt'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.13'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
26
+ spec.add_development_dependency 'byebug', '~> 9.0'
27
+ spec.add_development_dependency 'webmock', '~> 2.3'
28
+ spec.add_development_dependency 'rubocop', '~> 0.47'
29
+
30
+ spec.add_runtime_dependency 'thor', '~> 0.19'
31
+ spec.add_runtime_dependency 'netrc', '~> 0.11'
32
+ spec.add_runtime_dependency 'highline', '~> 1.7'
33
+ spec.add_runtime_dependency 'typhoeus', '~> 1.1'
34
+ spec.add_runtime_dependency 'thor-zsh_completion', '~> 0.1'
35
+ end
@@ -0,0 +1 @@
1
+ require 'klimt/clients/gravity_client'
@@ -0,0 +1,136 @@
1
+ require 'netrc'
2
+ require 'highline'
3
+ require 'typhoeus'
4
+ require 'json'
5
+
6
+ module Klimt
7
+ class GravityClient
8
+ attr_reader :token
9
+
10
+ HOSTS = { production: 'api.artsy.net', staging: 'stagingapi.artsy.net' }.freeze
11
+ DEFAULT_PAGE_SIZE = 20
12
+
13
+ def initialize(env:)
14
+ @host = host_from_environment(env)
15
+ @token = find_or_create_token
16
+ end
17
+
18
+ def find(type:, id:)
19
+ uri = "https://#{@host}/api/v1/#{type}/#{id}"
20
+ response = Typhoeus.get(uri, headers: headers)
21
+ response.body
22
+ end
23
+
24
+ def list(type:, params: [])
25
+ params = parse_params(params)
26
+ uri = "https://#{@host}/api/v1/#{type}"
27
+ response = Typhoeus.get(uri, headers: headers, params: params)
28
+ response.body
29
+ end
30
+
31
+ def count(type:, params: [])
32
+ params = parse_params(params)
33
+ params[:size] = 0
34
+ params[:total_count] = true
35
+ uri = "https://#{@host}/api/v1/#{type}"
36
+ response = Typhoeus.get(uri, headers: headers, params: params)
37
+ response.headers['X-Total-Count']
38
+ end
39
+
40
+ def search(term:, params: [], indexes: nil)
41
+ params = parse_params(params)
42
+ params[:term] = term
43
+ params[:indexes] = indexes unless indexes.nil?
44
+ uri = "https://#{@host}/api/v1/match"
45
+ response = Typhoeus.get(uri, headers: headers, params: params, params_encoding: :rack) # encode arrays correctly
46
+ response.body
47
+ end
48
+
49
+ # partners
50
+
51
+ def partner_locations(id:, params: [])
52
+ params = parse_params(params)
53
+ uri = "https://#{@host}/api/v1/partner/#{id}/locations"
54
+ response = Typhoeus.get(uri, headers: headers, params: params)
55
+ response.body
56
+ end
57
+
58
+ def partner_locations_count(id:, params: [])
59
+ params = parse_params(params).merge(size: 0, total_count: true)
60
+ uri = "https://#{@host}/api/v1/partner/#{id}/locations"
61
+ response = Typhoeus.get(uri, headers: headers, params: params)
62
+ response.headers['X-Total-Count']
63
+ end
64
+
65
+ def partner_near(params: [])
66
+ params = parse_params(params)
67
+ raise ArgumentError, 'a "near=LNG,LAT" parameter is required' unless params.include? 'near'
68
+ uri = "https://#{@host}/api/v1/partners"
69
+ response = Typhoeus.get(uri, headers: headers, params: params)
70
+ response.body
71
+ end
72
+
73
+ private
74
+
75
+ # Turn this from the command line:
76
+ # ["size=10", "page=2"]
77
+ # into this for Typhoeus:
78
+ # {'size' => 10, 'page' => 2}
79
+ #
80
+ def parse_params(params)
81
+ Hash[params.map { |pair| pair.split('=') }]
82
+ end
83
+
84
+ def headers
85
+ {
86
+ 'User-Agent' => "Klimt #{Klimt::VERSION}",
87
+ 'Content-type' => 'application/json',
88
+ 'Accept' => 'application/json',
89
+ 'X-ACCESS-TOKEN' => @token
90
+ }
91
+ end
92
+
93
+ def host_from_environment(env)
94
+ HOSTS[env.to_sym]
95
+ end
96
+
97
+ def find_or_create_token
98
+ _user, token = Netrc.read[@host]
99
+ token || generate_token
100
+ end
101
+
102
+ def generate_token
103
+ email, pass = ask_for_credentials
104
+ params = {
105
+ client_id: ENV['KLIMT_ID'],
106
+ client_secret: ENV['KLIMT_SECRET'],
107
+ grant_type: 'credentials',
108
+ email: email,
109
+ password: pass
110
+ }
111
+ response = Typhoeus.get "https://#{@host}/oauth2/access_token", params: params
112
+ body = JSON.parse(response.body)
113
+ quit "Login failed: #{body['error_description']}" unless response.success?
114
+ body['access_token'].tap do |new_token|
115
+ netrc = Netrc.read
116
+ netrc[@host] = email, new_token
117
+ netrc.save
118
+ end
119
+ end
120
+
121
+ def quit(msg)
122
+ $stderr.puts msg
123
+ exit 1
124
+ end
125
+
126
+ def ask_for_credentials
127
+ cli = HighLine.new
128
+ cli.say 'No login credentials found in .netrc'
129
+ cli.say 'Please login now'
130
+ cli.say '-----'
131
+ email = cli.ask('Artsy email : ') {}
132
+ pass = cli.ask('Artsy password : ') { |q| q.echo = 'x' }
133
+ [email, pass]
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,69 @@
1
+ require 'thor'
2
+ require 'thor/zsh_completion'
3
+ require 'klimt/commands/rendering'
4
+ require 'klimt/commands/partner'
5
+ require 'klimt/commands/city'
6
+
7
+ module Klimt
8
+ class Command < Thor
9
+ include Klimt::Commands::Rendering
10
+ include ZshCompletion::Command
11
+
12
+ map %w(--version -v) => 'version'
13
+
14
+ class_option :env, desc: 'Choose environment', default: 'production', aliases: ['-e'], enum: %w(production staging)
15
+ class_option :color, desc: 'Colorize output (via jq)', default: false, aliases: ['-c'], type: :boolean
16
+
17
+ desc 'find TYPE ID', 'An instance of the given TYPE, identified by ID'
18
+ def find(type, id)
19
+ client = GravityClient.new(env: options[:env])
20
+ response = client.find(type: type, id: id)
21
+ render response
22
+ end
23
+
24
+ desc 'list TYPE [PARAMS]', 'A list of the given TYPE, optionally filtered by PARAMS'
25
+ def list(type, *params)
26
+ client = GravityClient.new(env: options[:env])
27
+ response = client.list(type: type, params: params)
28
+ render response
29
+ end
30
+
31
+ desc 'count TYPE [PARAMS]', 'A count of the given TYPE, optionally filtered by PARAMS'
32
+ def count(type, *params)
33
+ client = GravityClient.new(env: options[:env])
34
+ count = client.count(type: type, params: params)
35
+ puts count
36
+ end
37
+
38
+ desc 'search TERM', 'Search results for the given TERM, optionally filtered by PARAMS'
39
+ method_option :lucky, type: :boolean, desc: 'Feeling lucky? Just summarize the top hit', default: false
40
+ method_option :indexes, type: :array, desc: 'An array of indexes to search', banner: 'Profile Artist etc...',
41
+ enum: %w(Article Artist Artist Artwork City Fair Feature Gene PartnerShow Profile Sale Tag)
42
+ def search(term, *params)
43
+ if options[:lucky]
44
+ params << 'size=1'
45
+ jq_filter = '.[0] | { model, id, display }'
46
+ end
47
+ indexes = options[:indexes] unless options[:indexes].nil?
48
+
49
+ client = GravityClient.new(env: options[:env])
50
+ response = client.search(term: term, params: params, indexes: indexes)
51
+ render response, jq_filter: jq_filter
52
+ end
53
+
54
+ desc 'version', 'print the version'
55
+ def version
56
+ puts Klimt::VERSION
57
+ end
58
+
59
+ # partner subcommands
60
+
61
+ desc 'partner', 'View subcommands that pertain to partners'
62
+ subcommand 'partner', Klimt::Commands::Partner
63
+
64
+ # city subcommands
65
+
66
+ desc 'city', 'View subcommands that pertain to cities'
67
+ subcommand 'city', Klimt::Commands::City
68
+ end
69
+ end
@@ -0,0 +1,19 @@
1
+ module Klimt
2
+ module Commands
3
+ class City < Thor
4
+ include Rendering
5
+
6
+ desc 'list', 'List all currently geocoded cities from S3'
7
+ method_option :featured, desc: 'Restrict to just "featured" cities', type: :boolean
8
+ method_option :short, desc: 'Show compact output', type: :boolean
9
+ def list
10
+ file = options[:featured] ? 'featured-cities.json' : 'cities.json'
11
+ uri = "http://artsy-geodata.s3-website-us-east-1.amazonaws.com/partner-cities/#{file}"
12
+ response = Typhoeus.get(uri)
13
+ jq_filter = options[:short] ? '.[] | { full_name, coords }' : '.'
14
+ jq_options = options[:short] ? '-c' : ''
15
+ render response.body, jq_filter: jq_filter, jq_options: jq_options
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,135 @@
1
+ require 'klimt/util/calculations'
2
+
3
+ module Klimt
4
+ module Commands
5
+ class Partner < Thor
6
+ include Rendering
7
+
8
+ CHECK = "\u2705".freeze
9
+ CROSS = "\u274C".freeze
10
+ RADIUS = '75km'.freeze
11
+
12
+ desc 'locations PARTNER_ID [PARAMS]', 'Locations for a given partner, optionally filtered by PARAMS'
13
+ method_option :private, desc: 'Also include where publicly_viewable=false', type: :boolean
14
+ def locations(partner_id, *params)
15
+ params << 'private=true' if options[:private]
16
+ client = GravityClient.new(env: options[:env])
17
+ response = client.partner_locations(id: partner_id, params: params)
18
+ render response
19
+ end
20
+
21
+ desc 'near', 'Partners near a lat/lng point'
22
+ method_option :y, type: :numeric, required: true, desc: 'Latitude'
23
+ method_option :x, type: :numeric, required: true, desc: 'Longitude'
24
+ method_option :eligible, type: :boolean, desc: 'Only where eligible_for_listing=true'
25
+ method_option :type, desc: 'Filter only institutions or galleries', banner: 'PartnerInstitution | PartnerGallery'
26
+ def near(*params)
27
+ params << "near=#{options[:y]},#{options[:x]}"
28
+ params << 'eligible_for_listing=true' if options[:eligible]
29
+ client = GravityClient.new(env: options[:env])
30
+ response = client.partner_near(params: params)
31
+ render response
32
+ end
33
+
34
+ desc 'check-locations [PARTNER_ID]', "Check correctness of a partner's location settings"
35
+ method_option :y, type: :numeric, desc: 'Optionally, check only near given point'
36
+ method_option :x, type: :numeric, desc: 'Optionally, check only near given point'
37
+ def check_locations(partner_id)
38
+ coords = options.values_at :y, :x
39
+ if coords.any? && !coords.all?
40
+ $stderr.puts 'Must provide both X and Y'
41
+ exit 1
42
+ end
43
+ point = coords.all? ? coords.join(',') : nil
44
+ client = GravityClient.new(env: options[:env])
45
+ response = client.find(type: 'partner', id: partner_id)
46
+ partner = JSON.parse(response)
47
+
48
+ # subscriber or institution ?
49
+ if institution?(partner)
50
+ puts "#{CHECK} Institution"
51
+ elsif gallery?(partner) && current_subscriber?(partner)
52
+ puts "#{CHECK} Gallery with current subscription"
53
+ else
54
+ puts "#{CROSS} Neither Institution nor current subscriber Gallery"
55
+ exit 1
56
+ end
57
+
58
+ # pubished locations?
59
+ public_count = published_locations_count(partner)
60
+ if public_count.zero?
61
+ puts "#{CROSS} No publicly viewable locations"
62
+ exit 1
63
+ else
64
+ puts "#{CHECK} #{public_count} publicly viewable locations"
65
+ end
66
+
67
+ return if point.nil?
68
+ # close enough to desired coords?
69
+ near_count = published_locations_count(partner, point: point)
70
+ if near_count.zero?
71
+ puts "#{CROSS} No publicly viewable locations within #{RADIUS} of #{point}"
72
+ report_distances(partner, point: point)
73
+ exit 1
74
+ else
75
+ puts "#{CHECK} #{near_count} publicly viewable locations within #{RADIUS} of #{point}"
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def gallery?(partner)
82
+ partner['type'] == 'Gallery'
83
+ end
84
+
85
+ def institution?(partner)
86
+ partner['type'] == 'Institution'
87
+ end
88
+
89
+ def current_subscriber?(partner)
90
+ client = GravityClient.new(env: options[:env])
91
+ params = [
92
+ "partner_id=#{partner['id']}",
93
+ 'current=true'
94
+ ]
95
+ response = client.count(type: 'partner_subscriptions', params: params)
96
+ count = response.to_i
97
+ count.positive?
98
+ end
99
+
100
+ def published_locations_count(partner, point: nil)
101
+ client = GravityClient.new(env: options[:env])
102
+ params = []
103
+ params << "near=#{point}" if point
104
+ response = client.partner_locations_count(id: partner['id'], params: params)
105
+ response.to_i
106
+ end
107
+
108
+ def unpublished_locations_count(partner, point: nil)
109
+ client = GravityClient.new(env: options[:env])
110
+ params = ['private=true']
111
+ params << "near=#{point}" if point
112
+ response = client.partner_locations_count(id: partner['id'], params: params)
113
+ total_count = response.to_i
114
+ published_count = published_locations_count(partner, point: point)
115
+ (total_count - published_count)
116
+ end
117
+
118
+ def report_distances(partner, point:)
119
+ params = ['private=true', 'size=100']
120
+ client = GravityClient.new(env: options[:env])
121
+ response = client.partner_locations(id: partner['id'], params: params)
122
+ locations = JSON.parse(response)
123
+ target_y, target_x = point.split(',').map(&:to_f)
124
+ puts " Partner location distances from #{target_y}, #{target_x}"
125
+ puts ' ---------------------------------------------------------------------------'
126
+ locations.each do |location|
127
+ name, city, pub = location.values_at 'name', 'city', 'publicly_viewable'
128
+ loc_y, loc_x = location['coordinates'].values_at('lat', 'lng')
129
+ dist = Calculations.spherical_distance([loc_y, loc_x], [target_y, target_x])
130
+ puts ' %20.20s | %15.15s | %4s | %7.1f km | (%+.4f, %+.4f)' % [name, city, (pub ? 'pub' : 'priv'), dist, loc_y, loc_x]
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,32 @@
1
+ module Klimt
2
+ module Commands
3
+ module Rendering
4
+ def render(obj, jq_options: nil, jq_filter: nil)
5
+ if jq_installed?
6
+ render_with_jq(obj, opts: jq_options, filter: jq_filter)
7
+ else
8
+ render_pretty(obj)
9
+ end
10
+ end
11
+
12
+ def render_with_jq(obj, opts: nil, filter: nil)
13
+ opts ||= ''
14
+ opts << ' -C' if options[:color]
15
+ filter ||= '.'
16
+ IO.popen("jq #{opts} \"#{filter}\"", 'r+') do |p|
17
+ p.write obj
18
+ p.close_write
19
+ puts p.read
20
+ end
21
+ end
22
+
23
+ def render_pretty(obj)
24
+ puts JSON.pretty_generate JSON.parse(obj)
25
+ end
26
+
27
+ def jq_installed?
28
+ !`command -v jq`.empty?
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ include Math
2
+ module Calculations
3
+ # Haversine formula for spherical distance
4
+ # See: https://rosettacode.org/wiki/Haversine_formula#Ruby
5
+
6
+ RADIUS = 6371 # rough radius of the Earth, in kilometers
7
+
8
+ def self.spherical_distance(start_latlng, end_latlng)
9
+ lat1, long1 = deg2rad(*start_latlng)
10
+ lat2, long2 = deg2rad(*end_latlng)
11
+ 2 * RADIUS * asin(sqrt(sin((lat2 - lat1) / 2)**2 + cos(lat1) * cos(lat2) * sin((long2 - long1) / 2)**2))
12
+ end
13
+
14
+ def self.deg2rad(lat, long)
15
+ [lat * PI / 180, long * PI / 180]
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Klimt
2
+ VERSION = '0.4.0'.freeze
3
+ end
data/lib/klimt.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'klimt/version'
2
+ require 'klimt/command'
3
+ require 'klimt/client'
4
+
5
+ module Klimt
6
+ # Your code goes here...
7
+ end
metadata ADDED
@@ -0,0 +1,224 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: klimt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Anandaroop Roy
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-02-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '9.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '9.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.47'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.47'
97
+ - !ruby/object:Gem::Dependency
98
+ name: thor
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.19'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.19'
111
+ - !ruby/object:Gem::Dependency
112
+ name: netrc
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.11'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.11'
125
+ - !ruby/object:Gem::Dependency
126
+ name: highline
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.7'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.7'
139
+ - !ruby/object:Gem::Dependency
140
+ name: typhoeus
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.1'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.1'
153
+ - !ruby/object:Gem::Dependency
154
+ name: thor-zsh_completion
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.1'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.1'
167
+ description:
168
+ email:
169
+ - anandaroop.roy+github@gmail.com
170
+ executables:
171
+ - klimt
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - ".gitignore"
176
+ - ".rspec"
177
+ - ".rubocop"
178
+ - ".rubocop.yml"
179
+ - ".rubocop_todo.yml"
180
+ - ".travis.yml"
181
+ - CODE_OF_CONDUCT.md
182
+ - Gemfile
183
+ - LICENSE.txt
184
+ - README.md
185
+ - Rakefile
186
+ - bin/console
187
+ - bin/setup
188
+ - circle.yml
189
+ - exe/klimt
190
+ - klimt.gemspec
191
+ - lib/klimt.rb
192
+ - lib/klimt/client.rb
193
+ - lib/klimt/clients/gravity_client.rb
194
+ - lib/klimt/command.rb
195
+ - lib/klimt/commands/city.rb
196
+ - lib/klimt/commands/partner.rb
197
+ - lib/klimt/commands/rendering.rb
198
+ - lib/klimt/util/calculations.rb
199
+ - lib/klimt/version.rb
200
+ homepage: https://github.com/anandaroop/klimt
201
+ licenses:
202
+ - MIT
203
+ metadata: {}
204
+ post_install_message:
205
+ rdoc_options: []
206
+ require_paths:
207
+ - lib
208
+ required_ruby_version: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: '0'
213
+ required_rubygems_version: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - ">="
216
+ - !ruby/object:Gem::Version
217
+ version: '0'
218
+ requirements: []
219
+ rubyforge_project:
220
+ rubygems_version: 2.5.1
221
+ signing_key:
222
+ specification_version: 4
223
+ summary: CLI for the Artsy API
224
+ test_files: []