aliyun-odps 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +31 -0
  4. data/Gemfile +3 -0
  5. data/README.md +55 -12
  6. data/Rakefile +15 -5
  7. data/aliyun-odps.gemspec +22 -11
  8. data/bin/console +10 -3
  9. data/lib/aliyun/odps.rb +69 -2
  10. data/lib/aliyun/odps/authorization.rb +90 -0
  11. data/lib/aliyun/odps/client.rb +40 -0
  12. data/lib/aliyun/odps/configuration.rb +16 -0
  13. data/lib/aliyun/odps/error.rb +97 -0
  14. data/lib/aliyun/odps/http.rb +138 -0
  15. data/lib/aliyun/odps/list.rb +40 -0
  16. data/lib/aliyun/odps/model/function.rb +16 -0
  17. data/lib/aliyun/odps/model/functions.rb +113 -0
  18. data/lib/aliyun/odps/model/instance.rb +130 -0
  19. data/lib/aliyun/odps/model/instance_task.rb +30 -0
  20. data/lib/aliyun/odps/model/instances.rb +119 -0
  21. data/lib/aliyun/odps/model/projects.rb +73 -0
  22. data/lib/aliyun/odps/model/resource.rb +26 -0
  23. data/lib/aliyun/odps/model/resources.rb +144 -0
  24. data/lib/aliyun/odps/model/table.rb +37 -0
  25. data/lib/aliyun/odps/model/table_column.rb +13 -0
  26. data/lib/aliyun/odps/model/table_partition.rb +9 -0
  27. data/lib/aliyun/odps/model/table_partitions.rb +90 -0
  28. data/lib/aliyun/odps/model/table_schema.rb +13 -0
  29. data/lib/aliyun/odps/model/tables.rb +125 -0
  30. data/lib/aliyun/odps/model/task_result.rb +9 -0
  31. data/lib/aliyun/odps/modelable.rb +16 -0
  32. data/lib/aliyun/odps/project.rb +47 -0
  33. data/lib/aliyun/odps/service_object.rb +27 -0
  34. data/lib/aliyun/odps/struct.rb +126 -0
  35. data/lib/aliyun/odps/tunnel/download_session.rb +98 -0
  36. data/lib/aliyun/odps/tunnel/router.rb +15 -0
  37. data/lib/aliyun/odps/tunnel/snappy_reader.rb +19 -0
  38. data/lib/aliyun/odps/tunnel/snappy_writer.rb +45 -0
  39. data/lib/aliyun/odps/tunnel/table_tunnels.rb +81 -0
  40. data/lib/aliyun/odps/tunnel/upload_block.rb +9 -0
  41. data/lib/aliyun/odps/tunnel/upload_session.rb +132 -0
  42. data/lib/aliyun/odps/utils.rb +102 -0
  43. data/lib/aliyun/odps/version.rb +1 -1
  44. data/requirements.png +0 -0
  45. data/wiki/error.md +188 -0
  46. data/wiki/functions.md +39 -0
  47. data/wiki/get_start.md +34 -0
  48. data/wiki/installation.md +15 -0
  49. data/wiki/instances.md +32 -0
  50. data/wiki/projects.md +51 -0
  51. data/wiki/resources.md +62 -0
  52. data/wiki/ssl.md +7 -0
  53. data/wiki/tables.md +75 -0
  54. data/wiki/tunnels.md +80 -0
  55. metadata +195 -13
  56. data/requirements.mindnode/QuickLook/Preview.jpg +0 -0
  57. data/requirements.mindnode/contents.xml +0 -10711
  58. data/requirements.mindnode/viewState.plist +0 -0
@@ -0,0 +1,16 @@
1
+ require 'addressable/uri'
2
+
3
+ module Aliyun
4
+ module Odps
5
+ class Configuration
6
+ attr_accessor :access_key, :secret_key, :endpoint, :tunnel_endpoint, :project, :options, :ssl_ca_file
7
+ def initialize
8
+ @options = {}
9
+ end
10
+
11
+ def protocol
12
+ Addressable::URI.parse(@endpoint).scheme
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,97 @@
1
+ module Aliyun
2
+ module Odps
3
+ class Error < StandardError; end
4
+
5
+ # [Aliyun::Odps::RequestError] when Odps give a Non 2xx response
6
+ class RequestError < Error
7
+ # Error Code defined by Odps
8
+ attr_reader :code
9
+
10
+ # Error Message defined by Odps
11
+ attr_reader :message
12
+
13
+ # It's the UUID to uniquely identifies this request;
14
+ # When you can't solve the problem, you can request help from the ODPS development engineer with the RequestId.
15
+ attr_reader :request_id
16
+
17
+ # The Origin Httparty Response
18
+ attr_reader :origin_response
19
+
20
+ def initialize(response)
21
+ assign_error_code(response)
22
+ assign_request_id(response)
23
+ @origin_response = response
24
+ super("#{@request_id} - #{@code}: #{@message}")
25
+ end
26
+
27
+ def assign_error_code(response)
28
+ result = response.parsed_response
29
+ if result.key?('Error')
30
+ @code = result['Error']['Code']
31
+ @message = result['Error']['Message']
32
+ elsif result.key?('Code')
33
+ @code = result['Code']
34
+ @message = result['Message']
35
+ end
36
+ end
37
+
38
+ def assign_request_id(response)
39
+ @request_id = response.headers['x-odps-request-id']
40
+ end
41
+ end
42
+
43
+ class XmlElementMissingError < Error
44
+ def initialize(element)
45
+ super("Missing #{element} Element in xml")
46
+ end
47
+ end
48
+
49
+ class MissingProjectConfigurationError < Error
50
+ def initialize
51
+ super("Must config project first. Use Aliyun::Odps.configure {|config| config.project = 'your-project' }")
52
+ end
53
+ end
54
+
55
+ class PriorityInvalidError < Error
56
+ def initialize
57
+ super('Priority must more than or equal to zero.')
58
+ end
59
+ end
60
+
61
+ class InstanceTaskNotSuccessError < Error
62
+ def initialize(name, status, task_result)
63
+ super("Task #{name} #{status}: #{task_result}")
64
+ end
65
+ end
66
+
67
+ class InstanceNameInvalidError < Error
68
+ def initialize(name)
69
+ super("#{name} should match pattern: #{Instance::NAME_PATTERN}")
70
+ end
71
+ end
72
+
73
+ class TunnelEndpointMissingError < Error
74
+ def initialize
75
+ super("Tunnel Endpoint auto detect fail, Use Aliyun::Odps.configure {|config| config.tunnel_endpoint = 'your-project' } to config")
76
+ end
77
+ end
78
+
79
+ class ValueNotSupportedError < Error
80
+ def initialize(attr, supported_value)
81
+ super("#{attr} only support: #{Utils.wrap(supported_value).join(', ')} !!")
82
+ end
83
+ end
84
+
85
+ class ResourceMissingContentError < Error
86
+ def initialize
87
+ super('A Resource must exist file or table')
88
+ end
89
+ end
90
+
91
+ class RecordNotMatchSchemaError < Error
92
+ def initialize(values, schema)
93
+ super("#{values} not match with #{schema}")
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,138 @@
1
+ require 'httparty'
2
+ require 'addressable/uri'
3
+ require 'aliyun/odps/error'
4
+
5
+ module Aliyun
6
+ module Odps
7
+ class Http # nodoc
8
+ include HTTParty
9
+
10
+ class BetterXmlParser < HTTParty::Parser
11
+ protected
12
+
13
+ def xml
14
+ MultiXml.parse(body)
15
+ rescue
16
+ body
17
+ end
18
+ end
19
+ parser BetterXmlParser
20
+
21
+ attr_reader :config
22
+
23
+ def initialize(config)
24
+ @config = config
25
+ end
26
+
27
+ def get(uri, options = {})
28
+ request('GET', uri, options)
29
+ end
30
+
31
+ def put(uri, options = {})
32
+ headers = default_content_type.merge(options[:headers] || {})
33
+ request('PUT', uri, options.merge(headers: headers))
34
+ end
35
+
36
+ def post(uri, options = {})
37
+ headers = default_content_type.merge(options[:headers] || {})
38
+ request('POST', uri, options.merge(headers: headers))
39
+ end
40
+
41
+ def delete(uri, options = {})
42
+ headers = default_content_type.merge(options[:headers] || {})
43
+ request('DELETE', uri, options.merge(headers: headers))
44
+ end
45
+
46
+ def options(uri, options = {})
47
+ request('OPTIONS', uri, options)
48
+ end
49
+
50
+ def head(uri, options = {})
51
+ request('HEAD', uri, options)
52
+ end
53
+
54
+ private
55
+
56
+ def request(verb, resource, options = {})
57
+ query = options.fetch(:query, {})
58
+ headers = options.fetch(:headers, {})
59
+ body = options.delete(:body)
60
+
61
+ append_headers!(headers, verb, body, options.merge(path: resource))
62
+ path = config.endpoint + resource
63
+ options = { headers: headers, query: query, body: body }
64
+ append_options!(options, path)
65
+
66
+ wrap(self.class.__send__(verb.downcase, path, options))
67
+ end
68
+
69
+ def wrap(response)
70
+ case response.code
71
+ when 200..299
72
+ response
73
+ else
74
+ fail RequestError, response
75
+ end
76
+ end
77
+
78
+ def append_headers!(headers, verb, body, options)
79
+ append_default_headers!(headers)
80
+ append_body_headers!(headers, body)
81
+ append_authorization_headers!(headers, verb, options)
82
+ end
83
+
84
+ def append_options!(options, url)
85
+ options.merge!(uri_adapter: Addressable::URI)
86
+ if config.ssl_ca_file
87
+ options.merge!(ssl_ca_file: config.ssl_ca_file)
88
+ elsif url.start_with?('https://')
89
+ options.merge!(verify_peer: true)
90
+ end
91
+ end
92
+
93
+ def append_default_headers!(headers)
94
+ headers.merge!(default_headers)
95
+ end
96
+
97
+ def append_body_headers!(headers, body)
98
+ return headers unless body
99
+
100
+ unless headers.key?('Content-MD5')
101
+ headers.merge!('Content-MD5' => Utils.md5_hexdigest(body))
102
+ end
103
+
104
+ return if headers.key?('Content-Length')
105
+ headers.merge!('Content-Length' => Utils.content_size(body).to_s)
106
+ end
107
+
108
+ def append_authorization_headers!(headers, verb, options)
109
+ auth_key = get_auth_key(
110
+ options.merge(verb: verb, headers: headers, date: headers['Date'])
111
+ )
112
+ headers.merge!('Authorization' => auth_key)
113
+ end
114
+
115
+ def get_auth_key(options)
116
+ Authorization.get_authorization(config.access_key, config.secret_key, options)
117
+ end
118
+
119
+ def default_headers
120
+ {
121
+ 'User-Agent' => user_agent,
122
+ 'Date' => Time.now.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
123
+ }
124
+ end
125
+
126
+ def default_content_type
127
+ {
128
+ 'Content-Type' => 'application/xml'
129
+ }
130
+ end
131
+
132
+ def user_agent
133
+ "aliyun-odps-sdk-ruby/#{Aliyun::Odps::VERSION} " \
134
+ "(#{RbConfig::CONFIG['host_os']} ruby-#{RbConfig::CONFIG['ruby_version']})"
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,40 @@
1
+ require 'forwardable'
2
+
3
+ module Aliyun
4
+ module Odps
5
+ # Wrap for simple array and give marker and max_items methods
6
+ class List
7
+ include Enumerable
8
+ extend Forwardable
9
+
10
+ attr_reader :marker, :max_items
11
+ def_delegators :@objects, :[], :each, :size, :inspect
12
+
13
+ def initialize(marker, max_items, objects)
14
+ @marker = marker
15
+ @max_items = max_items.to_i
16
+ @objects = objects
17
+ end
18
+
19
+ # Auto detect marker, max_items, values from result,
20
+ # build a object, where you can access marker, max_items, and values
21
+ #
22
+ # @example
23
+ #
24
+ # Aliyun::Odps::List.build(result, %w(Projects Project)) do |hash|
25
+ # Project.new(hash.merge(client: client))
26
+ # end
27
+ #
28
+ # @return [List]
29
+ def self.build(result, keys, &_block)
30
+ top_key = keys.first
31
+ marker = Utils.dig_value(result, top_key, 'Marker')
32
+ max_items = Utils.dig_value(result, top_key, 'MaxItems')
33
+ objects = Utils.wrap(Utils.dig_value(result, *keys)).map do |hash|
34
+ yield hash
35
+ end
36
+ new(marker, max_items, objects)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ module Aliyun
2
+ module Odps
3
+ class Function < Struct::Base
4
+ extend Aliyun::Odps::Modelable
5
+
6
+ property :name, String, required: true
7
+ property :owner, String
8
+ property :class_type, String
9
+ property :creation_time, DateTime
10
+ property :resources, Array
11
+ property :location, String
12
+
13
+ alias_method :alias=, :name=
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,113 @@
1
+ module Aliyun
2
+ module Odps
3
+ # Methods for Functions
4
+ class Functions < ServiceObject
5
+ # List Functions of project
6
+ #
7
+ # @see http://repo.aliyun.com/api-doc/Function/get_functions/index.html Get functions
8
+ #
9
+ # @param options [Hash] options
10
+ # @option options [String] :name specify function name
11
+ # @option options [String] :owner specify function owner
12
+ # @option options [String] :marker
13
+ # @option options [String] :maxitems (1000)
14
+ #
15
+ # @return [List]
16
+ def list(options = {})
17
+ Utils.stringify_keys!(options)
18
+ path = "/projects/#{project.name}/registration/functions"
19
+ query = Utils.hash_slice(options, 'name', 'owner', 'marker', 'maxitems')
20
+ result = client.get(path, query: query).parsed_response
21
+
22
+ Aliyun::Odps::List.build(result, %w(Functions Function)) do |hash|
23
+ Function.new(hash)
24
+ end
25
+ end
26
+
27
+ # Get Function
28
+ #
29
+ # @param name specify function name
30
+ #
31
+ # @return [Function]
32
+ def get(name)
33
+ path = "/projects/#{project.name}/registration/functions/#{name}"
34
+
35
+ result = client.get(path).parsed_response
36
+ Function.new(Utils.dig_value(result, 'Function'))
37
+ end
38
+ alias_method :function, :get
39
+
40
+ # Register function in project
41
+ #
42
+ # @see http://repo.aliyun.com/api-doc/Function/post_function/index.html Post function
43
+ #
44
+ # @param name [String] specify function name
45
+ # @param class_path [String] specify class Path used by function
46
+ # @param resources [Array<Model::Resource>] specify resources used by function
47
+ #
48
+ # @return [Function]
49
+ def create(name, class_path, resources = [])
50
+ path = "/projects/#{project.name}/registration/functions"
51
+
52
+ function = Function.new(
53
+ name: name,
54
+ class_type: class_path,
55
+ resources: resources
56
+ )
57
+
58
+ resp = client.post(path, body: build_create_body(function))
59
+
60
+ function.tap do |obj|
61
+ obj.location = resp.headers['Location']
62
+ end
63
+ end
64
+
65
+ # Update function in project
66
+ #
67
+ # @see http://repo.aliyun.com/api-doc/Function/put_function/index.html Put function
68
+ #
69
+ # @param name [String] specify function name
70
+ # @param class_path [String] specify class Path used by function
71
+ # @param resources [Array<Model::Resource>] specify resources used by function
72
+ #
73
+ # @return [true]
74
+ def update(name, class_path, resources = [])
75
+ path = "/projects/#{project.name}/registration/functions/#{name}"
76
+
77
+ function = Function.new(
78
+ name: name,
79
+ class_type: class_path,
80
+ resources: resources
81
+ )
82
+ !!client.put(path, body: build_create_body(function))
83
+ end
84
+
85
+ # Delete function in project
86
+ #
87
+ # @see http://repo.aliyun.com/api-doc/Function/delete_function/index.html Delete function
88
+ #
89
+ # @param name [String] specify function name
90
+ #
91
+ # @return [true]
92
+ def delete(name)
93
+ path = "/projects/#{project.name}/registration/functions/#{name}"
94
+ !!client.delete(path)
95
+ end
96
+
97
+ private
98
+
99
+ def build_create_body(function)
100
+ fail XmlElementMissingError, 'ClassType' if function.class_type.nil?
101
+ fail XmlElementMissingError, 'Resources' if function.resources.empty?
102
+
103
+ Utils.to_xml(
104
+ 'Function' => {
105
+ 'Alias' => function.name,
106
+ 'ClassType' => function.class_type,
107
+ 'Resources' => function.resources.map(&:to_hash)
108
+ }
109
+ )
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,130 @@
1
+ module Aliyun
2
+ module Odps
3
+ class Instance < Struct::Base
4
+ extend Aliyun::Odps::Modelable
5
+
6
+ NAME_PATTERN = /^([a-z]|[A-Z]){1,}([a-z]|[A-Z]|[\d]|_)*/
7
+
8
+ property :project, Project, required: true
9
+
10
+ property :name, String, required: true
11
+ property :owner, String
12
+ property :comment, String
13
+ property :priority, Integer
14
+ property :tasks, Array
15
+ property :status, String
16
+ property :start_time, DateTime
17
+ property :end_time, DateTime
18
+ property :location, String
19
+
20
+ # Get task detail of instance
21
+ #
22
+ # @see http://repo.aliyun.com/api-doc/Instance/get_instance_detail/index.html Get instance detail
23
+ #
24
+ # @params task_name [String] specify task name
25
+ #
26
+ # @return [Hash]
27
+ def task_detail(task_name)
28
+ path = "/projects/#{project.name}/instances/#{name}"
29
+ query = { instancedetail: true, taskname: task_name }
30
+ client.get(path, query: query).parsed_response
31
+ end
32
+
33
+ # Get task progress of instance
34
+ #
35
+ # @see http://repo.aliyun.com/api-doc/Instance/get_instance_progress/index.html Get instance progress
36
+ #
37
+ # @params task_name [String] specify task name
38
+ #
39
+ # @return [Hash]
40
+ def task_progress(task_name)
41
+ path = "/projects/#{project.name}/instances/#{name}"
42
+ query = { instanceprogress: true, taskname: task_name }
43
+ client.get(path, query: query).parsed_response['Progress']
44
+ end
45
+
46
+ # Get task summary of instance
47
+ #
48
+ # @see http://repo.aliyun.com/api-doc/Instance/get_instance_summary/index.html Get instance summary
49
+ #
50
+ # @params task_name [String] specify task name
51
+ #
52
+ # @return [Hash]
53
+ def task_summary(task_name)
54
+ path = "/projects/#{project.name}/instances/#{name}"
55
+ query = { instancesummary: true, taskname: task_name }
56
+ client.get(path, query: query).parsed_response
57
+ end
58
+
59
+ # Get task results
60
+ #
61
+ # @return [Hash<name, TaskResult>]
62
+ def task_results
63
+ path = "/projects/#{project.name}/instances/#{name}"
64
+ query = { result: true }
65
+ result = client.get(path, query: query).parsed_response
66
+ task_results = Utils.dig_value(result, 'Instance', 'Tasks', 'Task')
67
+ Hash[Utils.wrap(task_results).map { |v| [v['Name'], Aliyun::Odps::TaskResult.new(v)] }]
68
+ end
69
+
70
+ # Get tasks of instance
71
+ #
72
+ # @see http://repo.aliyun.com/api-doc/Instance/get_instance_task/index.html Get instance task
73
+ #
74
+ # @return [List]
75
+ def list_tasks
76
+ path = "/projects/#{project.name}/instances/#{name}"
77
+ query = { taskstatus: true }
78
+ result = client.get(path, query: query).parsed_response
79
+
80
+ keys = %w(Instance Tasks Task)
81
+ Utils.wrap(Utils.dig_value(result, *keys)).map do |hash|
82
+ InstanceTask.new(hash)
83
+ end
84
+ end
85
+
86
+ # Terminate the instance
87
+ #
88
+ # @see http://repo.aliyun.com/api-doc/Instance/put_instance_terminate/index.html Put instance terminated
89
+ #
90
+ # @return true
91
+ def terminate
92
+ path = "/projects/#{project.name}/instances/#{name}"
93
+
94
+ body = Utils.to_xml(
95
+ 'Instance' => { 'Status' => 'Terminated' }
96
+ )
97
+ !!client.put(path, body: body)
98
+ end
99
+
100
+ # Get status
101
+ #
102
+ # @see http://repo.aliyun.com/api-doc/Instance/get_instance/index.html Get instance
103
+ #
104
+ # @return [String] Instance status: Suspended, Running, Terminated
105
+ def get_status
106
+ path = "/projects/#{project.name}/instances/#{name}"
107
+ result = client.get(path).parsed_response
108
+ Utils.dig_value(result, 'Instance', 'Status')
109
+ end
110
+
111
+ # Block process until instance success
112
+ #
113
+ # @raise [InstanceTaskNotSuccessError] if task not success
114
+ def wait_for_success(interval = 0.01)
115
+ wait_for_terminated(interval)
116
+
117
+ list_tasks.each do |task|
118
+ if task.status.upcase != 'SUCCESS'
119
+ fail InstanceTaskNotSuccessError.new(task.name, task.status, task_results[task.name].result)
120
+ end
121
+ end
122
+ end
123
+
124
+ # Block process until instance terminated
125
+ def wait_for_terminated(interval = 0.01)
126
+ sleep interval while get_status != 'Terminated'
127
+ end
128
+ end
129
+ end
130
+ end