aliyun-odps 0.1.0 → 0.4.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.
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