dm_cloud 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dm_cloud.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Jeremy Mortelette
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # DmCloud
2
+
3
+ I created this gem to simplify request and responses from DailyMotion Cloud API.
4
+ With this gem, you can :
5
+ - get generated embed code as a string
6
+ - get direct access url to your files (I used this to provide video flux to TV-connected application)
7
+ - (I'm working on ) video creation, delete, paginated lists and video informations (a/v encodings, bitrate, video lenght...)
8
+ - (I'm working on ) CRUD on videos' meta-data
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'dm_cloud'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install dm_cloud
23
+
24
+ ## Usage
25
+
26
+ First, your will need to specify your :user_id, :api_key and your security level.
27
+ I used a file in `APP_ROOT/config/initializers/conf.rb`.
28
+ You can note the securitylevel, for more information about it, take a look at ``
29
+
30
+ # DAILYMOTION CLOUD SETTINGS
31
+ require 'dm_cloud'
32
+ DMC_USER_ID = 'your user id'
33
+ DMC_SECRET = 'your api key'
34
+ DMC_SECURITY_LEVEL = :none
35
+
36
+ DMCloud.configure( {
37
+ :user_key => DMC_USER_ID,
38
+ :secret_key => DMC_SECRET,
39
+ :security_level => DMC_SECURITY_LEVEL
40
+ })
41
+
42
+
43
+
44
+ Second part, how to get you embed url :
45
+
46
+ DMCloud::Streaming.embed('your video id looks like a secret key')
47
+
48
+ Or how to get your direct url :
49
+
50
+ DMCloud::Streaming.url('your video id', ['asset_name'], {options})
51
+
52
+ The next parts will come soon, just need some time to finish its and create corresponding tests.
53
+
54
+ ## Contributing
55
+
56
+ Your welcome to share and enhance this gem.
57
+ This is my first one (and not the last one) but I know some mistakes might be done by myself.
58
+ I do my best and I'm open to all ideas or comments about my work.
59
+
60
+ 1. Fork it
61
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
62
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
63
+ 4. Push to the branch (`git push origin my-new-feature`)
64
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/dm_cloud.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dm_cloud/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "dm_cloud"
8
+ gem.version = DMCloud::VERSION
9
+ gem.authors = ["Jeremy Mortelette"]
10
+ gem.email = ["mortelette.jeremy@gmail.com"]
11
+ gem.description = 'This gem will simplify usage of DailyMotion Cloud API, it represent api in ruby style, with automated handler for search and upload files'
12
+ gem.summary = 'Simplify DailyMotion Cloud API usage'
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,72 @@
1
+ module DMCloud
2
+ class Media
3
+ # Creates a new media object.
4
+ # This method can either create an empty media object
5
+ # or also download a media with the url paramater
6
+ # and use it as the source to encode the ASSET_NAME listed in assets_names
7
+ # Params :
8
+ # args:
9
+ # url: SCHEME://USER:PASSWORD@HOSTNAME/MY/PATH/FILENAME.EXTENSION (could be ftp or http)
10
+ # author: an author name
11
+ # title: a title for the film
12
+ # assets_names: (Array) – (optional) the list of ASSET_NAME you want to transcode,
13
+ # when you set this parameter you must also set the url parameter
14
+ # Return :
15
+ # media_id: return the media id of the object
16
+ def self.create(media_id)
17
+ call = "media.create"
18
+
19
+ params = {
20
+ call: call,
21
+ args: DMCloud::Builder::Media.create(args)
22
+ }
23
+ DMCloud::Request.execute(call, params)
24
+ end
25
+
26
+ # Delete a media object with all its associated assets.
27
+ #
28
+ # Parameters:
29
+ # id (media ID) – (required) the id of the media object you want to delete.
30
+ # Return :
31
+ # Nothing
32
+ def self.delete
33
+ call = "media.delete"
34
+
35
+ params = {
36
+ call: call,
37
+ args: { id: media_id}
38
+ }
39
+ DMCloud::Request.execute(call, params)
40
+ end
41
+
42
+ def self.info(fields = [])
43
+ call = "media.info"
44
+
45
+ params = {
46
+ call: call,
47
+ args: DMCloud::Builder::Media.info(fields)
48
+ }
49
+ DMCloud::Request.execute(call, params)
50
+ end
51
+
52
+ # Gives information about a given media object.
53
+ #
54
+ # Params :
55
+ # media_id: (media ID) – (required) the id of the new media object.
56
+ # fields (Array) – (required) the list of fields to retrieve.
57
+ # Returns: a multi-level structure containing about the media related to the requested fields.
58
+ Return type: Object
59
+ def self.list(options = {})
60
+ call = "media.list"
61
+ page = options[:page].present? ? options[:page] : 1
62
+ per_page = options[:per_page].present? ? options[:per_page] : 10
63
+
64
+ params = {
65
+ call: call,
66
+ args: DMCloud::Builder::Media.list(options)
67
+ }
68
+ DMCloud::Request.execute(call, params)
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,13 @@
1
+ module DMCloud
2
+ class Request
3
+
4
+
5
+ def self.execute(call, params = {})
6
+ request = DMCloud.identify(params)
7
+ params.merge!({'auth' => request})
8
+ result = DMCloud::Request.new(params)
9
+ DMCloud::Response.parse(call, result)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,146 @@
1
+ module DMCloud
2
+ class Signing
3
+ # To sign a URL, the client needs a secret shared with Dailymotion Cloud.
4
+ # This secret is call client secret and is available in the back-office interface.
5
+ # Params:
6
+ # expires: An expiration timestamp.
7
+ # sec-level: A security level mask.
8
+ # url-no-query: The URL without the query-string.
9
+ # nonce: A 8 characters-long random alphanumeric lowercase string to make the signature unique.
10
+ # secret: The client secret.
11
+ # sec-data: If sec-level doesn’t have the DELEGATED bit activated,
12
+ # this component contains concatenated informations
13
+ # for all activated sec levels.
14
+ # pub-sec-data: Some sec level data have to be passed in clear in the signature.
15
+ # To generate this component the parameters are serialized using x-www-form-urlencoded, compressed with gzip and encoded in base64.
16
+ # Result :
17
+ # return a string which contain the signed url like
18
+ # <url>?auth=<expires>-<sec>-<nonce>-<md5sum>[-<pub-sec-data>]
19
+ def self.sign(stream)
20
+ raise StandardError, "missing :stream in params" unless stream
21
+ security = security(DMCloud.config[:security_level])
22
+ sec_data = security_data(DMCloud.config[:security_level])
23
+
24
+ base = {
25
+ :sec_level => security(DMCloud.config[:security_level]),
26
+ :url_no_query => stream,
27
+ :expires => (Time.now + 1.hour).to_i,
28
+ :nonce => SecureRandom.hex(16)[0,8],
29
+ :secret => DMCloud.config[:secret_key]
30
+ }
31
+ base.merge!(:sec_data => sec_data, :pub_sec_data => sec_data) unless sec_data.nil?
32
+ puts base
33
+ digest_struct = build_digest_struct(base)
34
+
35
+ check_sum = Digest::MD5.hexdigest(digest_struct)
36
+
37
+ signed_url = [base[:expires], base[:sec_level], base[:nonce], check_sum].compact
38
+ signed_url.merge!(:pub_sec_data => sec_data) unless sec_data.nil?
39
+
40
+ puts signed_url
41
+
42
+ signed_url = signed_url.join('-')
43
+ signed_url
44
+ end
45
+
46
+ # Prepare datas for signing
47
+ # Params :
48
+ # base : contains media id and others for url signing
49
+ def self.build_digest_struct(base)
50
+ result = []
51
+ base.each_pair { |key, value| result << value }
52
+ result.join('')
53
+ end
54
+
55
+ # The client must choose a security level for the signature.
56
+ # Security level defines the mechanism used by Dailymotion Cloud architecture
57
+ # to ensure the signed URL will be used by a single end-user.
58
+ # Params :
59
+ # type :
60
+ # None: The signed URL will be valid for everyone
61
+ # ASNUM: The signed URL will only be valid for the AS of the end-user.
62
+ # The ASNUM (for Autonomous System Number) stands for the network identification,
63
+ # each ISP have a different ASNUM for instance.
64
+ # IP: The signed URL will only be valid for the IP of the end-user.
65
+ # This security level may wrongly block some users
66
+ # which have their internet access load-balanced between several proxies.
67
+ # This is the case in some office network or some ISPs.
68
+ # User-Agent: Used in addition to one of the two former levels,
69
+ # this level a limit on the exact user-agent of the end-user.
70
+ # This is more secure but in some specific condition may lead to wrongly blocked users.
71
+ # Use Once: The signed URL will only be usable once.
72
+ # Note: should not be used with stream URLs.
73
+ # Country: The URL can only be queried from specified countrie(s).
74
+ # The rule can be reversed to allow all countries except some.
75
+ # Referer: The URL can only be queried
76
+ # if the Referer HTTP header contains a specified value.
77
+ # If the URL contains a Referer header with a different value,
78
+ # the request is refused. If the Referer header is missing,
79
+ # the request is accepted in order to prevent from false positives as some browsers,
80
+ # anti-virus or enterprise proxies may remove this header.
81
+ # Delegate: This option instructs the signing algorithm
82
+ # that security level information won’t be embeded into the signature
83
+ # but gathered and lock at the first use.
84
+ # Result :
85
+ # Return a string which contain the signed url like
86
+ # http://cdn.dmcloud.net/route/<user_id>/<media_id>/<asset_name>.<asset_extension>?auth=<auth_token>
87
+ def self.security(type = nil)
88
+ type = :none unless type
89
+ type = type.to_sym if type.class == String
90
+
91
+ result = case type
92
+ when :none
93
+ 0 # None
94
+ when :delegate
95
+ 1 << 0 # None
96
+ when :asnum
97
+ 1 << 1 # The number part of the end-user AS prefixed by the ‘AS’ string (ie: as=AS41690)
98
+ when :ip
99
+ 1 << 2 # The end-user quad dotted IP address (ie: ip=195.8.215.138)
100
+ when :user_agent
101
+ 1 << 3 # The end-user browser user-agent (parameter name is ua)
102
+ when :use_once
103
+ 1 << 4 # None
104
+ when :country
105
+ 1 << 5 # A list of 2 characters long country codes in lowercase by comas. If the list starts with a dash, the rule is inverted (ie: cc=fr,gb,de or cc=-fr,it). This data have to be stored in pub-sec-data component
106
+ when :referer
107
+ 1 << 6 # A list of URL prefixes separated by spaces stored in the pub-sec-data component (ex: rf=http;//domain.com/a/+http:/domain.com/b/).
108
+ end
109
+ result
110
+ end
111
+
112
+ def self.security_data(type, value = nil)
113
+ type = type.to_sym if type.class == String
114
+
115
+ result = case type
116
+ when :asnum
117
+ "as=#{value}" # The number part of the end-user AS prefixed by the ‘AS’ string (ie: as=AS41690)
118
+ when :ip
119
+ "ip=#{value}" # The end-user quad dotted IP address (ie: ip=195.8.215.138)
120
+ when :user_agent
121
+ "ua=#{value}" # The end-user browser user-agent (parameter name is ua)
122
+ when :country
123
+ "cc=#{value}" # A list of 2 characters long country codes in lowercase by comas. If the list starts with a dash, the rule is inverted (ie: cc=fr,gb,de or cc=-fr,it). This data have to be stored in pub-sec-data component
124
+ when :referer
125
+ "rf=#{value}" # A list of URL prefixes separated by spaces stored in the pub-sec-data component (ex: rf=http;//domain.com/a/+http:/domain.com/b/).
126
+ else
127
+ nil
128
+ end
129
+ result
130
+ end
131
+
132
+ def self.security_pub_sec_data(type, value)
133
+ type = type.to_sym if type.class == String
134
+
135
+ result = case type
136
+ when :country
137
+ "cc=#{value}" # A list of 2 characters long country codes in lowercase by comas. If the list starts with a dash, the rule is inverted (ie: cc=fr,gb,de or cc=-fr,it). This data have to be stored in pub-sec-data component
138
+ when :referer
139
+ "rf=#{value}" # A list of URL prefixes separated by spaces stored in the pub-sec-data component (ex: rf=http;//domain.com/a/+http:/domain.com/b/).
140
+ else
141
+ nil
142
+ end
143
+ result
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,67 @@
1
+ require "time"
2
+ require "openssl"
3
+ require "base64"
4
+ require 'digest/md5'
5
+
6
+ module DMCloud
7
+ class Streaming
8
+ # Default URL to get embed content ou direct url
9
+ DIRECT_STREAM = '[PROTOCOL]://cdn.dmcloud.net/route/[USER_ID]/[MEDIA_ID]/[ASSET_NAME].[ASSET_EXTENSION]'
10
+ EMBED_STREAM = '[PROTOCOL]://api.dmcloud.net/embed/[USER_ID]/[MEDIA_ID]?auth=[AUTH_TOKEN]&skin=[SKIN_ID]'
11
+ EMBED_IFRAME = '<iframe width=[WIDTH] height=[HEIGHT] frameborder="0" scrolling="no" src="[EMBED_URL]"></iframe>'
12
+ # Get embeded player
13
+ # Params :
14
+ # media_id: this is the id of the media (eg: 4c922386dede830447000009)
15
+ # options:
16
+ # skin_id: (optional) the id of the custom skin for the video player
17
+ # width: (optional) the width for the video player frame
18
+ # height: (optional) the height for the video player frame
19
+ # Result :
20
+ # return a string which contain the signed url like
21
+ # <iframe width="848" height="480" frameborder="0" scrolling="no" src="http://api.dmcloud.net/embed/<user_id>/<media_id>?auth=<auth_token>&skin=<skin_id>"></iframe>
22
+ def self.embed(media_id, options = {})
23
+ raise StandardError, "missing :media_id in params" unless media_id
24
+
25
+ skin_id = options[:skin_id].present? ? options[:skin_id] : 'modern1'
26
+ width = options[:width].present? ? options[:width] : '848'
27
+ height = options[:height].present? ? options[:height] : '480'
28
+
29
+ stream = EMBED_STREAM
30
+ stream.gsub!('[PROTOCOL]', DMCloud.config[:protocol])
31
+ stream.gsub!('[USER_ID]', DMCloud.config[:user_key])
32
+ stream.gsub!('[MEDIA_ID]', media_id)
33
+ stream.gsub!('[SKIN_ID]', skin_id)
34
+ stream += '?auth=[AUTH_TOKEN]'.gsub!('[AUTH_TOKEN]', DMCloud::Signing.sign(stream))
35
+
36
+ frame = EMBED_IFRAME
37
+ frame.gsub!('[WIDTH]', width)
38
+ frame.gsub!('[HEIGHT]', height)
39
+ frame.gsub!('[EMBED_URL]', stream)
40
+ frame
41
+ end
42
+
43
+ # Get media url for direct link to the file on DailyMotion Cloud
44
+ # Params :
45
+ # media_id: this is the id of the media (eg: 4c922386dede830447000009)
46
+ # asset_name: the name of the asset you want to stream (eg: mp4_h264_aac)
47
+ # asset_extension: the extension of the asset, most of the time it is the first part of the asset name (eg: mp4)
48
+ # Result :
49
+ # return a string which contain the signed url like
50
+ # http://cdn.dmcloud.net/route/<user_id>/<media_id>/<asset_name>.<asset_extension>?auth=<auth_token>
51
+ def self.url(media_id, asset_name, asset_extension = nil)
52
+ asset_extension = asset_name.split('_').first unless asset_extension
53
+
54
+ raise StandardError, "missing :media_id in params" unless media_id
55
+ raise StandardError, "missing :asset_name in params" unless asset_name
56
+
57
+ stream = DIRECT_STREAM
58
+ stream.gsub!('[PROTOCOL]', DMCloud.config[:protocol])
59
+ stream.gsub!('[USER_ID]', DMCloud.config[:user_key])
60
+ stream.gsub!('[MEDIA_ID]', media_id)
61
+ stream.gsub!('[ASSET_NAME]', asset_name)
62
+ stream.gsub!('[ASSET_EXTENSION]', asset_extension)
63
+ stream += '?auth=[AUTH_TOKEN]'.gsub!('[AUTH_TOKEN]', DMCloud::Signing.sign(stream))
64
+ stream
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,3 @@
1
+ module DMCloud
2
+ VERSION = "0.0.1"
3
+ end
data/lib/dm_cloud.rb ADDED
@@ -0,0 +1,83 @@
1
+ require "dm_cloud/version"
2
+ require 'yaml'
3
+
4
+ module DMCloud
5
+
6
+ # Configuration defaults
7
+ @@config = {
8
+ :security_level => 'none',
9
+ :protocol => 'http'
10
+ }
11
+
12
+ YAML_INITIALIZER_PATH = File.dirname(__FILE__)
13
+ @valid_config_keys = @@config.keys
14
+
15
+ # Configure through hash
16
+ def self.configure(opts = {})
17
+ opts.each {|k,v| @@config[k.to_sym] = v } # if @valid_config_keys.include? k.to_sym}
18
+ end
19
+
20
+ # Configure through yaml file
21
+ def self.configure_with(yaml_file_path = nil)
22
+ yaml_file_path = YAML_INITIALIZER_PATH unless yaml_file_path
23
+ begin
24
+ config = YAML::load(IO.read(path_to_yaml_file))
25
+ rescue Errno::ENOENT
26
+ log(:warning, "YAML configuration file couldn't be found. Using defaults."); return
27
+ rescue Psych::SyntaxError
28
+ log(:warning, "YAML configuration file contains invalid syntax. Using defaults."); return
29
+ end
30
+
31
+ configure(config)
32
+ end
33
+
34
+ def self.config
35
+ @@config = configure unless @@config
36
+ @@config
37
+ end
38
+
39
+ def self.identify(request)
40
+ user_id = @@config[:user_id]
41
+ api_key = @@config[:api_key]
42
+ checksum = md5(user_id + normalize(request) + api_key)
43
+
44
+ auth_token = user_id + ':' + checksum
45
+ end
46
+
47
+ def self.create_has_library(library)
48
+ define_singleton_method("has_#{library}?") do
49
+ cv="@@#{library}"
50
+ if !class_variable_defined? cv
51
+ begin
52
+ require library.to_s
53
+ class_variable_set(cv,true)
54
+ rescue LoadError
55
+ class_variable_set(cv,false)
56
+ end
57
+ end
58
+ class_variable_get(cv)
59
+ end
60
+ end
61
+
62
+ create_has_library :treaming
63
+
64
+
65
+ class << self
66
+ # Load a object saved on a file.
67
+ def load(filename)
68
+ if File.exists? filename
69
+ o=false
70
+ File.open(filename,"r") {|fp| o=Marshal.load(fp) }
71
+ o
72
+ else
73
+ false
74
+ end
75
+ end
76
+ end
77
+
78
+ autoload(:Streaming, 'dm_cloud/streaming')
79
+
80
+
81
+ end
82
+
83
+ Dir.glob('dm_cloud/**/*.rb').each{ |m| require File.dirname(__FILE__) + '/dm_cloud/' + m }
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dm_cloud
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jeremy Mortelette
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-25 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: This gem will simplify usage of DailyMotion Cloud API, it represent api
15
+ in ruby style, with automated handler for search and upload files
16
+ email:
17
+ - mortelette.jeremy@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - Gemfile
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - dm_cloud.gemspec
28
+ - lib/dm_cloud.rb
29
+ - lib/dm_cloud/media.rb
30
+ - lib/dm_cloud/request.rb
31
+ - lib/dm_cloud/signing.rb
32
+ - lib/dm_cloud/streaming.rb
33
+ - lib/dm_cloud/version.rb
34
+ homepage: ''
35
+ licenses: []
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 1.8.24
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: Simplify DailyMotion Cloud API usage
58
+ test_files: []