yandex_client 0.1.1 → 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.
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexClient
4
+ module ErrorHandler
5
+ private
6
+
7
+ def error_for_response(response)
8
+ msg = response.body.empty? ? response.status.to_s : response.body.to_s.force_encoding('UTF-8')
9
+ err =
10
+ case response.code
11
+ when 404
12
+ NotFoundError.new(msg, http_code: 404)
13
+ else
14
+ ApiRequestError.new(msg, http_code: response.code)
15
+ end
16
+
17
+ raise err
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexClient
4
+ # https://tech.yandex.ru/passport/doc/dg/reference/request-docpage/
5
+ #
6
+ # Example:
7
+ # YandexClient::Passport::Client[Token.first.access_token].info
8
+ class Passport
9
+ include Configurable
10
+ include ErrorHandler
11
+
12
+ ACTION_URL = 'https://login.yandex.ru/info'
13
+
14
+ attr_reader :token
15
+
16
+ class << self
17
+ def with_token(token)
18
+ new(token)
19
+ end
20
+
21
+ alias [] with_token
22
+ end
23
+
24
+ def initialize(token)
25
+ @token = token
26
+ end
27
+
28
+ def info
29
+ process_response \
30
+ with_config.get(ACTION_URL, headers: auth_headers)
31
+ end
32
+
33
+ private
34
+
35
+ def auth_headers
36
+ {Authorization: "Bearer #{@token}"}
37
+ end
38
+
39
+ def process_response(response)
40
+ return JSON.parse(response.body, symbolize_names: true) if response.status.success?
41
+
42
+ error_for_response(response)
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YandexClient
4
- VERSION = '0.1.1'
4
+ VERSION = '1.1.0'
5
5
  end
data/lib/yandex_client.rb CHANGED
@@ -2,31 +2,50 @@
2
2
 
3
3
  require 'yandex_client/version'
4
4
 
5
- module YandexClient
6
- Config = Struct.new(:api_key, :api_secret, :logger)
5
+ require 'yandex_client/error_handler'
6
+ require 'yandex_client/configurable'
7
7
 
8
- def self.config
9
- @config ||= Config.new
10
- end
8
+ require 'yandex_client/dav'
9
+ require 'yandex_client/disk'
10
+ require 'yandex_client/auth'
11
+ require 'yandex_client/passport'
11
12
 
12
- def self.configure
13
- yield config
13
+ module YandexClient
14
+ Config = Struct.new \
15
+ :api_key,
16
+ :api_secret,
17
+ :logger,
18
+ :read_timeout,
19
+ :write_timeout,
20
+ :connect_timeout
21
+
22
+ class ApiRequestError < StandardError
23
+ attr_reader :http_code
24
+
25
+ def initialize(msg = nil, http_code: nil)
26
+ if http_code
27
+ super "#{msg}, (http code #{http_code})"
28
+ else
29
+ super msg
30
+ end
31
+
32
+ @http_code = http_code
33
+ end
14
34
  end
15
35
 
16
- autoload :Client, 'yandex_client/client'
17
- autoload :ApiRequestError, 'yandex_client/api_request_error'
18
- autoload :NotFoundError, 'yandex_client/not_found_error'
36
+ NotFoundError = Class.new(ApiRequestError)
19
37
 
20
- module Auth
21
- autoload :Client, 'yandex_client/auth/client'
22
- end
38
+ class << self
39
+ def config
40
+ @config ||= Config.new(nil, nil, nil, 120, 600, 5)
41
+ end
23
42
 
24
- module Passport
25
- autoload :Client, 'yandex_client/passport/client'
26
- end
43
+ def configure
44
+ yield config
45
+ end
27
46
 
28
- module Dav
29
- autoload :Client, 'yandex_client/dav/client'
30
- autoload :PropfindParser, 'yandex_client/dav/propfind_parser'
47
+ def auth
48
+ @auth ||= Auth.new
49
+ end
31
50
  end
32
51
  end
@@ -23,12 +23,13 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ['lib']
25
25
 
26
- spec.add_runtime_dependency 'oj'
26
+ spec.add_runtime_dependency 'http'
27
27
  spec.add_runtime_dependency 'ox'
28
28
 
29
29
  spec.add_development_dependency 'bundler'
30
30
  spec.add_development_dependency 'rake', '>= 10'
31
31
  spec.add_development_dependency 'rspec', '>= 3.0'
32
+ spec.add_development_dependency 'simplecov', '>= 0.9'
32
33
  spec.add_development_dependency 'vcr'
33
34
  spec.add_development_dependency 'webmock'
34
35
 
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yandex_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maxim Tretyakov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-12-23 00:00:00.000000000 Z
11
+ date: 2021-12-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: oj
14
+ name: http
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0.9'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0.9'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: vcr
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -115,9 +129,11 @@ executables: []
115
129
  extensions: []
116
130
  extra_rdoc_files: []
117
131
  files:
132
+ - ".github/workflows/main.yml"
118
133
  - ".gitignore"
134
+ - ".pryrc"
119
135
  - ".rspec"
120
- - ".travis.yml"
136
+ - ".rubocop.yml"
121
137
  - Gemfile
122
138
  - LICENSE.txt
123
139
  - README.md
@@ -125,17 +141,17 @@ files:
125
141
  - bin/console
126
142
  - bin/setup
127
143
  - dip.yml
128
- - docker/Dockerfile.2.6
129
- - docker/docker-compose.development.yml
144
+ - docker/Dockerfile.dip
130
145
  - docker/docker-compose.yml
146
+ - docker/prepare_env.sh
131
147
  - lib/yandex_client.rb
132
- - lib/yandex_client/api_request_error.rb
133
- - lib/yandex_client/auth/client.rb
134
- - lib/yandex_client/client.rb
135
- - lib/yandex_client/dav/client.rb
136
- - lib/yandex_client/dav/propfind_parser.rb
137
- - lib/yandex_client/not_found_error.rb
138
- - lib/yandex_client/passport/client.rb
148
+ - lib/yandex_client/auth.rb
149
+ - lib/yandex_client/configurable.rb
150
+ - lib/yandex_client/dav.rb
151
+ - lib/yandex_client/dav/prop_find_response.rb
152
+ - lib/yandex_client/disk.rb
153
+ - lib/yandex_client/error_handler.rb
154
+ - lib/yandex_client/passport.rb
139
155
  - lib/yandex_client/version.rb
140
156
  - yandex_client.gemspec
141
157
  homepage: https://github.com/yamax2/yandex_client
@@ -157,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
173
  - !ruby/object:Gem::Version
158
174
  version: '0'
159
175
  requirements: []
160
- rubygems_version: 3.0.6
176
+ rubygems_version: 3.2.5
161
177
  signing_key:
162
178
  specification_version: 4
163
179
  summary: Yandex Client
data/.travis.yml DELETED
@@ -1,12 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.6.5
5
- before_install: gem install bundler
6
- before_script:
7
- - bundle install
8
- script: bundle exec rspec
9
- notifications:
10
- email:
11
- on_success: never
12
- on_failure: always
@@ -1,13 +0,0 @@
1
- FROM ruby:2.6.5-alpine
2
-
3
- RUN apk add --update --no-cache tzdata openssh-client less git build-base
4
- RUN cp /usr/share/zoneinfo/Asia/Yekaterinburg /etc/localtime && echo 'Asia/Yekaterinburg' > /etc/timezone
5
-
6
- RUN ssh-keyscan -H github.com >> /etc/ssh/ssh_known_hosts
7
-
8
- ENV BUNDLE_APP_CONFIG /app/.bundle
9
-
10
- RUN echo 'gem: --no-rdoc --no-ri --no-document' > /root/.gemrc
11
- RUN gem install bundler -v '2.0.2'
12
-
13
- WORKDIR /app
@@ -1,18 +0,0 @@
1
- version: '2'
2
-
3
- services:
4
- app:
5
- volumes:
6
- - ..:/app
7
- - ../:/localgems
8
- - ssh-data:/ssh:ro
9
- - bundler-data:/bundle
10
-
11
- volumes:
12
- bundler-data:
13
- external:
14
- name: bundler_data
15
-
16
- ssh-data:
17
- external:
18
- name: ssh_data
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module YandexClient
4
- class ApiRequestError < StandardError
5
- attr_reader :error, :error_description, :code
6
-
7
- def initialize(error:, error_description:, code:)
8
- @error = error
9
- @error_description = error_description
10
- @code = code
11
-
12
- super [error, error_description, "http code #{@code}"].compact.join(', ')
13
- end
14
- end
15
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'uri'
4
-
5
- module YandexClient
6
- module Auth
7
- # https://tech.yandex.ru/oauth/doc/dg/reference/refresh-client-docpage/
8
- # https://tech.yandex.ru/oauth/doc/dg/reference/auto-code-client-docpage/#auto-code-client__get-token
9
- #
10
- # https://oauth.yandex.ru/authorize?response_type=code&client_id=99bcbd17ad7f411694710592d978a4a2&force_confirm=false
11
- #
12
- # Example:
13
- # token = Token.first
14
- #
15
- # client = YandexClient::Auth::Client.new
16
- # client.create_token(code: '9388894')
17
- # client.refresh_token(refresh_token: token.refresh_token)
18
- class Client < ::YandexClient::Client
19
- AUTH_ACTION_URL = 'https://oauth.yandex.ru/token'
20
-
21
- ACTIONS = {
22
- create_token: 'authorization_code',
23
- refresh_token: 'refresh_token'
24
- }.freeze
25
-
26
- private
27
-
28
- def http(_request_uri)
29
- @http ||= super
30
- end
31
-
32
- def http_method_for_action
33
- METHOD_POST
34
- end
35
-
36
- def request_body(params)
37
- body_hash = {
38
- grant_type: ACTIONS.fetch(@action),
39
- client_id: ::YandexClient.config.api_key,
40
- client_secret: ::YandexClient.config.api_secret
41
- }.merge!(params)
42
-
43
- URI.encode_www_form(body_hash)
44
- end
45
-
46
- def request_headers(_params)
47
- {
48
- 'Content-type' => 'application/x-www-form-urlencoded'
49
- }
50
- end
51
-
52
- def request_uri(_params)
53
- @request_uri ||= URI.parse(AUTH_ACTION_URL)
54
- end
55
- end
56
- end
57
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'oj'
4
- require 'net/http'
5
-
6
- module YandexClient
7
- # Base api client
8
- class Client
9
- DEFAULT_TIMEOUT = 10
10
- METHOD_GET = 'Get'
11
- METHOD_POST = 'Post'
12
-
13
- ERRORS_BY_CODES = {
14
- 404 => NotFoundError
15
- }.freeze
16
-
17
- def method_missing(method_name, *args, &_)
18
- return super unless self.class::ACTIONS.include?(method_name)
19
-
20
- @action = method_name
21
- result = nil
22
- response = make_request(args.last.is_a?(Hash) ? args.last : {})
23
-
24
- result = parse_response_body(response) unless response.body.nil? || response.body.empty?
25
- process_errors(response, result) unless response.is_a?(Net::HTTPSuccess)
26
-
27
- result
28
- end
29
-
30
- def respond_to_missing?(method_name, include_private = false)
31
- self.class::ACTIONS.include?(method_name) || super
32
- end
33
-
34
- private
35
-
36
- def http(request_uri)
37
- Net::HTTP.new(request_uri.host, request_uri.port).tap do |http|
38
- http.use_ssl = true
39
- http.read_timeout = DEFAULT_TIMEOUT
40
- http.continue_timeout = DEFAULT_TIMEOUT
41
- end
42
- end
43
-
44
- def http_method_for_action
45
- METHOD_GET
46
- end
47
-
48
- def log_api_request(request_uri, request, response)
49
- return if (logger = YandexClient.config.logger).nil?
50
-
51
- logger.info "#{request.method} #{request_uri} #{response.code} #{response.message}"
52
- logger.info "request headers: #{Oj.dump(request.to_hash)}"
53
- logger.info "response headers: #{Oj.dump(response.to_hash)}"
54
- end
55
-
56
- def make_request(params)
57
- request_uri = request_uri(params)
58
- @body = request_body(params)
59
-
60
- request = Object.const_get("Net::HTTP::#{http_method_for_action}").new(
61
- request_uri.request_uri,
62
- request_headers(params)
63
- )
64
-
65
- request.body = @body
66
- response = http(request_uri).start { |http| http.request(request) }
67
-
68
- log_api_request(request_uri, request, response)
69
-
70
- response
71
- end
72
-
73
- def parse_response_body(response)
74
- Oj.load(response.body, symbol_keys: true)
75
- end
76
-
77
- def process_errors(response, result)
78
- klass = ERRORS_BY_CODES.fetch(response.code.to_i, ApiRequestError)
79
-
80
- raise klass.new(
81
- error: result.is_a?(Hash) ? result&.fetch(:error) : result,
82
- error_description: result.is_a?(Hash) ? result&.fetch(:error_description) : nil,
83
- code: response.code.to_i
84
- )
85
- end
86
-
87
- def request_body(_params)
88
- end
89
-
90
- def request_headers(_params)
91
- {}
92
- end
93
-
94
- def request_uri(_params)
95
- raise 'not implemented'
96
- end
97
- end
98
- end
@@ -1,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'digest'
4
-
5
- module YandexClient
6
- module Dav
7
- # https://tech.yandex.ru/disk/doc/dg/reference/put-docpage/
8
- #
9
- # cli = YandexClient::Dav::Client.new(access_token: access_token)
10
- #
11
- # cli.put(file: '1.txt', name: '/a/b/c/1.txt')
12
- # cli.delete(name: '/a/b/c/1.txt')
13
- # cli.mkcol(name: '/a/b/c')
14
- #
15
- # cli.propfind(name: '/a/dip.yml', depth: 0)
16
- # cli.propfind(name: '/a', depth: 1)
17
- # cli.propfind(name: '/', quota: true)
18
- class Client < ::YandexClient::Client
19
- ACTION_URL = 'https://webdav.yandex.ru'
20
- PROPFIND_QUERY = '<?xml version="1.0" encoding="utf-8"?><propfind xmlns="DAV:"></propfind>'
21
- PROPFIND_QUOTA_QUERY = <<~XML
22
- <D:propfind xmlns:D="DAV:">
23
- <D:prop>
24
- <D:quota-available-bytes/>
25
- <D:quota-used-bytes/>
26
- </D:prop>
27
- </D:propfind>
28
- XML
29
-
30
- # actions with header processors
31
- ACTIONS = {
32
- mkcol: nil,
33
- delete: nil,
34
-
35
- put: lambda do |params|
36
- filename = params.fetch(:file)
37
-
38
- etag = params[:etag] || Digest::MD5.file(filename)
39
- sha256 = params[:sha256] || Digest::SHA256.file(filename)
40
- size = params[:size] || File.size(filename)
41
-
42
- {
43
- Etag: etag.to_s,
44
- Sha256: sha256.to_s,
45
- Expect: '100-continue',
46
- 'Content-Length' => size.to_s
47
- }
48
- end,
49
-
50
- propfind: lambda do |params|
51
- depth = 0
52
- depth = params.fetch(:depth, 0) unless params.key?(:quota)
53
-
54
- headers = {
55
- Depth: depth.to_s,
56
- 'Content-Type' => 'application/x-www-form-urlencoded'
57
- }
58
-
59
- headers['Content-Length'] = @body.length unless @body.nil? || @body.empty?
60
- headers
61
- end
62
- }.freeze
63
-
64
- BODY_PROCESSORS = {
65
- put: ->(params) { File.read(params.fetch(:file)) },
66
- propfind: lambda do |params|
67
- if params.fetch(:quota, false)
68
- PROPFIND_QUOTA_QUERY
69
- elsif params.fetch(:depth, 0).zero?
70
- PROPFIND_QUERY
71
- end
72
- end
73
- }.freeze
74
-
75
- RESPONSE_PARSERS = {
76
- propfind: PropfindParser
77
- }.freeze
78
-
79
- def initialize(access_token:)
80
- @access_token = access_token
81
- end
82
-
83
- private
84
-
85
- def http_method_for_action
86
- @action.to_s.capitalize
87
- end
88
-
89
- def parse_response_body(response)
90
- if response.is_a?(Net::HTTPSuccess) && !(parser = RESPONSE_PARSERS[@action]).nil?
91
- parser.call(response.body)
92
- else
93
- response.body
94
- end
95
- end
96
-
97
- def request_body(params)
98
- return if (proc = BODY_PROCESSORS[@action]).nil?
99
-
100
- proc.call(params)
101
- end
102
-
103
- def request_headers(params)
104
- proc = ACTIONS.fetch(@action)
105
-
106
- headers = {Authorization: "OAuth #{@access_token}"}
107
- headers.merge!(proc.call(params)) unless proc.nil?
108
-
109
- headers
110
- end
111
-
112
- def request_uri(params)
113
- URI.parse("#{ACTION_URL}#{params.fetch(:name)}")
114
- end
115
- end
116
- end
117
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'ox'
4
- require 'cgi'
5
-
6
- module YandexClient
7
- module Dav
8
- class PropfindParser
9
- SUCCESS_STATUS = 'HTTP/1.1 200 OK'
10
-
11
- PROCESSORS = {
12
- getcontentlength: ->(value) { value.to_i },
13
- resourcetype: ->(value) { value.is_a?(Hash) ? :folder : :file }
14
- }.freeze
15
-
16
- class ParseError < StandardError
17
- end
18
-
19
- def initialize(xml)
20
- # FIXME: dup?
21
- @document = Ox.load(xml.dup.force_encoding('UTF-8'), mode: :hash)
22
- end
23
-
24
- def call
25
- @document[:'d:multistatus'].each_with_object({}) do |node, memo|
26
- next if (response = node[:'d:response']).nil?
27
-
28
- name = CGI.unescape(response.fetch(:'d:href'))
29
- memo[name] = parse_node_props(response.fetch(:'d:propstat'))
30
- end
31
- end
32
-
33
- def self.call(xml)
34
- new(xml).call
35
- end
36
-
37
- private
38
-
39
- def parse_node_props(node)
40
- raise ParseError unless node.fetch(:'d:status') == SUCCESS_STATUS
41
-
42
- node.fetch(:'d:prop').each_with_object({}) do |(key, value), memo|
43
- prop_key = key.to_s.gsub(/^d:/, '').to_sym
44
- processor = PROCESSORS[prop_key]
45
-
46
- memo[prop_key] = processor.nil? ? value : processor.call(value)
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module YandexClient
4
- class NotFoundError < ApiRequestError
5
- end
6
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module YandexClient
4
- module Passport
5
- # https://tech.yandex.ru/passport/doc/dg/reference/request-docpage/
6
- #
7
- # Example:
8
- # client = YandexClient::Passport::Client.new(access_token: Token.first.access_token)
9
- # client.info
10
- class Client < ::YandexClient::Client
11
- ACTION_URL = 'https://login.yandex.ru/info'
12
- ACTIONS = %i[info].freeze
13
-
14
- # action - info
15
- def initialize(access_token:)
16
- @access_token = access_token
17
- end
18
-
19
- private
20
-
21
- def http(_request_uri)
22
- @http ||= super
23
- end
24
-
25
- def request_uri(_params)
26
- @request_uri ||= URI.parse(ACTION_URL)
27
- end
28
-
29
- def request_headers(_params)
30
- {
31
- Authorization: "Bearer #{@access_token}"
32
- }
33
- end
34
- end
35
- end
36
- end