typingpool 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +452 -0
  3. data/lib/typingpool/amazon/hit/assignment/empty.rb +19 -0
  4. data/lib/typingpool/amazon/hit/assignment.rb +43 -0
  5. data/lib/typingpool/amazon/hit/full/fromsearchhits.rb +44 -0
  6. data/lib/typingpool/amazon/hit/full.rb +105 -0
  7. data/lib/typingpool/amazon/hit.rb +458 -0
  8. data/lib/typingpool/amazon/question.rb +45 -0
  9. data/lib/typingpool/amazon.rb +3 -677
  10. data/lib/typingpool/app/cli/formatter.rb +16 -0
  11. data/lib/typingpool/app/cli.rb +64 -0
  12. data/lib/typingpool/app/friendlyexceptions.rb +34 -0
  13. data/lib/typingpool/app.rb +2 -97
  14. data/lib/typingpool/config/root.rb +114 -0
  15. data/lib/typingpool/config.rb +13 -119
  16. data/lib/typingpool/filer/audio.rb +84 -0
  17. data/lib/typingpool/filer/csv.rb +57 -0
  18. data/lib/typingpool/filer/dir.rb +76 -0
  19. data/lib/typingpool/filer/files/audio.rb +63 -0
  20. data/lib/typingpool/filer/files.rb +55 -0
  21. data/lib/typingpool/filer.rb +4 -313
  22. data/lib/typingpool/project/local.rb +117 -0
  23. data/lib/typingpool/project/remote/s3.rb +135 -0
  24. data/lib/typingpool/project/remote/sftp.rb +100 -0
  25. data/lib/typingpool/project/remote.rb +65 -0
  26. data/lib/typingpool/project.rb +2 -396
  27. data/lib/typingpool/template/assignment.rb +17 -0
  28. data/lib/typingpool/template/env.rb +77 -0
  29. data/lib/typingpool/template.rb +2 -87
  30. data/lib/typingpool/test/script.rb +310 -0
  31. data/lib/typingpool/test.rb +1 -306
  32. data/lib/typingpool/transcript/chunk.rb +129 -0
  33. data/lib/typingpool/transcript.rb +1 -125
  34. data/lib/typingpool/utility/castable.rb +65 -0
  35. data/lib/typingpool/utility.rb +1 -61
  36. data/test/test_integration_script_6_tp_finish.rb +1 -0
  37. metadata +135 -81
@@ -8,6 +8,8 @@ module Typingpool
8
8
  #project is also associated with audio files on a remote server.
9
9
  class Project
10
10
  require 'uri'
11
+ require 'typingpool/project/local'
12
+ require 'typingpool/project/remote'
11
13
 
12
14
  #Returns a time interval corresponding to the length of each audio
13
15
  #chunk within the project. (Each chunk may be transcribed
@@ -193,401 +195,5 @@ module Typingpool
193
195
  (1 - hms.count .. -1).each{|i| hms[i] = hms[i].to_s.rjust(2, '0') }
194
196
  hms.join(":")
195
197
  end
196
-
197
- #Representation of the Project instance on remote servers. This is
198
- #basically a collection of audio files to be transcribed and HTML
199
- #files containing instructions and a form for the
200
- #transcribers. The backend can be Amazon S3 (the default) or an
201
- #SFTP server. Each backend is encapsulated in its own subclass. A
202
- #backend subclass must provide a 'put' method, which takes an
203
- #array of IO streams and an optional array of remote file
204
- #basenames; a 'remove' method, which takes an array of remote file
205
- #basenames; and the methods 'host' and 'path', which return the
206
- #location of the destination server and destination directory,
207
- #respectively.
208
- #
209
- #Thus, there will always be 'put', 'remove', 'host' and 'path'
210
- #methods available, in addition to the Project::Remote methods
211
- #outlined below.
212
- class Remote
213
-
214
- #The project name
215
- attr_accessor :name
216
-
217
- #Constructor. Takes the project name and a Config
218
- #instance. Returns a Project::Remote::S3 or
219
- #Project::Remote::SFTP instance, depending on the particulars of
220
- #the Config. If there are sufficient config params to return
221
- #EITHER an S3 or SFTP subclass, it will prefer the SFTP
222
- #subclass.
223
- def self.from_config(name, config)
224
- if config.sftp
225
- SFTP.new(name, config.sftp)
226
- elsif config.amazon && config.amazon.bucket
227
- S3.new(name, config.amazon)
228
- else
229
- raise Error, "No valid upload params found in config file (SFTP or Amazon info)"
230
- end
231
- end
232
-
233
- #Like project.remote.remove, except it takes an array of URLs
234
- #instead an array of remote basenames, saving you from having to
235
- #manually extract basenames from the URL.
236
- def remove_urls(urls)
237
- basenames = urls.map{|url| url_basename(url) }
238
- remove(basenames){|file| yield(file) if block_given? }
239
- end
240
-
241
- #Given a file path, returns the URL to the file path were it to
242
- #be uploaded by this instance.
243
- def file_to_url(file)
244
- "#{@url}/#{URI.escape(file)}"
245
- end
246
-
247
- #Given an URL, returns the file portion of the path, given the
248
- #configuration of this instance.
249
- def url_basename(url)
250
- basename = url.split("#{self.url}/")[1] or raise Error, "Could not find base url '#{self.url}' within longer url '#{url}'"
251
- URI.unescape(basename)
252
- end
253
-
254
- #Subclass for storing remote files on Amazon Simple Storage
255
- #Service (S3)
256
- class S3 < Remote
257
- require 'aws/s3'
258
-
259
- #An Amazon Web Services "Access Key ID." Set from the
260
- #Config#amazon value passed to Project::Remote::S3.new, but
261
- #changeable.
262
- attr_accessor :key
263
-
264
- #An Amazon Web Services "Secret Access Key." Set from the
265
- #Config#amazon value passed to Project::Remote::S3.new, but
266
- #changeable.
267
- attr_accessor :secret
268
-
269
- #The S3 "bucket" where uploads will be stores. Set from the
270
- #Config#amazon value passed to Project::Remote::S3.new, but
271
- #changeable.
272
- attr_accessor :bucket
273
-
274
- #Returns the base URL, which is prepended to the remote
275
- #files. This is either the 'url' attribute of the
276
- #Config#amazon value passed to Project::Remote::S3.new or, if
277
- #that attribute is not set, the value returned by
278
- #'default_url' (e.g. "https://bucketname.s3.amazonaws.com").
279
- attr_reader :url
280
-
281
- #Constructor. Takes the project name and the result of calling
282
- #the 'amazon' method on a Config instance (i.e. the amazon
283
- #section of a Config file).
284
- def initialize(name, amazon_config)
285
- @name = name
286
- @config = amazon_config
287
- @key = @config.key or raise Error::File::Remote::S3, "Missing Amazon key in config"
288
- @secret = @config.secret or raise Error::File::Remote::S3, "Missing Amazon secret in config"
289
- @bucket = @config.bucket or raise Error::File::Remote::S3, "Missing Amazon bucket in config"
290
- @url = @config.url || default_url
291
- end
292
-
293
- #The remote host (server) name, parsed from #url
294
- def host
295
- URI.parse(@url).host
296
- end
297
-
298
- #The remote path (directory), pased from #url
299
- def path
300
- URI.parse(@url).path
301
- end
302
-
303
- #Upload files/strings to S3, optionally changing the names in the process.
304
- # ==== Params
305
- #[io_streams] Enumerable collection of IO objects, like a File
306
- # or StringIO instance.
307
- #[as] Optional if the io_streams are File instances. Array of
308
- # file basenames, used to name the destination
309
- # files. Default is the basename of the Files
310
- # passed in as io_streams.
311
- # ==== Returns
312
- #Array of URLs corresponding to the uploaded files.
313
- def put(io_streams, as=io_streams.map{|file| File.basename(file)})
314
- batch(io_streams) do |stream, i|
315
- dest = as[i]
316
- yield(stream, dest) if block_given?
317
- begin
318
- AWS::S3::S3Object.store(dest, stream, @bucket, :access => :public_read)
319
- rescue AWS::S3::NoSuchBucket
320
- make_bucket
321
- retry
322
- end
323
- file_to_url(dest)
324
- end #batch
325
- end
326
-
327
- #Delete objects from S3.
328
- # ==== Params
329
- #[files] Enumerable collection of file names. Should NOT
330
- # include the bucket name (path).
331
- # ==== Returns
332
- #Array of booleans corresponding to whether the delete call
333
- #succeeded.
334
- def remove(files)
335
- batch(files) do |file, i|
336
- yield(file) if block_given?
337
- AWS::S3::S3Object.delete(file, @bucket)
338
- end
339
- end
340
-
341
- protected
342
-
343
- def batch(io_streams)
344
- results = []
345
- io_streams.each_with_index do |stream, i|
346
- connect if i == 0
347
- begin
348
- results.push(yield(stream, i))
349
- rescue AWS::S3::S3Exception => e
350
- if e.message.match(/AWS::S3::SignatureDoesNotMatch/)
351
- raise Error::File::Remote::S3::Credentials, "S3 operation failed with a signature error. This likely means your AWS key or secret is wrong. Error: #{e}"
352
- else
353
- raise Error::File::Remote::S3, "Your S3 operation failed with an Amazon error: #{e}"
354
- end #if
355
- end #begin
356
- end #files.each
357
- disconnect unless io_streams.empty?
358
- results
359
- end
360
-
361
- def connect
362
- AWS::S3::Base.establish_connection!(
363
- :access_key_id => @key,
364
- :secret_access_key => @secret,
365
- :persistent => true,
366
- :use_ssl => true
367
- )
368
- end
369
-
370
- def disconnect
371
- AWS::S3::Base.disconnect
372
- end
373
-
374
- def make_bucket
375
- AWS::S3::Bucket.create(@bucket)
376
- end
377
-
378
- def default_url
379
- "https://#{@bucket}.s3.amazonaws.com"
380
- end
381
- end #S3
382
-
383
- #Subclass for storing remote files on an SFTP server. Only
384
- #public/private key authentication has been tested. There is not
385
- #yet any provision for password-based authentication, though
386
- #adding it should be trivial.
387
- class SFTP < Remote
388
- require 'net/sftp'
389
-
390
- #Returns the remote host (server) name. This is set from
391
- #Config#sftp#host.
392
- attr_reader :host
393
-
394
- #Returns the remote path (directory). This is set from
395
- #Config#sftp#path.
396
- attr_reader :path
397
-
398
- #Returns the name of the user used to log in to the SFTP
399
- #server. This is et from Config#sftp#user.
400
- attr_reader :user
401
-
402
- #Returns the base URL, which is prepended to the remote
403
- #files. This is set from Config#sftp#url.
404
- attr_reader :url
405
-
406
- #Constructor. Takes the project name and a Config#sftp.
407
- def initialize(name, sftp_config)
408
- @name = name
409
- @config = sftp_config
410
- @user = @config.user or raise Error::File::Remote::SFTP, "No SFTP user specified in config"
411
- @host = @config.host or raise Error::File::Remote::SFTP, "No SFTP host specified in config"
412
- @url = @config.url or raise Error::File::Remote::SFTP, "No SFTP url specified in config"
413
- @path = @config.path || ''
414
- end
415
-
416
- #See docs for Project::Remote::S3#put.
417
- def put(io_streams, as=io_streams.map{|file| File.basename(file)})
418
- begin
419
- i = 0
420
- batch(io_streams) do |stream, connection|
421
- dest = as[i]
422
- i += 1
423
- yield(stream, dest) if block_given?
424
- connection.upload(stream, join_with_path(dest))
425
- file_to_url(dest)
426
- end
427
- rescue Net::SFTP::StatusException => e
428
- raise Error::File::Remote::SFTP, "SFTP upload failed: #{e.description}"
429
- end
430
- end
431
-
432
- #See docs for Project::Remote::S3#remove.
433
- def remove(files)
434
- requests = batch(files) do |file, connection|
435
- yield(file) if block_given?
436
- connection.remove(join_with_path(file))
437
- end
438
- failures = requests.reject{|request| request.response.ok?}
439
- if not(failures.empty?)
440
- summary = failures.map{|request| request.response.to_s}.join('; ')
441
- raise Error::File::Remote::SFTP, "SFTP removal failed: #{summary}"
442
- end
443
- end
444
-
445
- protected
446
-
447
- def connection
448
- begin
449
- Net::SFTP.start(@host, @user) do |connection|
450
- yield(connection)
451
- connection.loop
452
- end
453
- rescue Net::SSH::AuthenticationFailed
454
- raise Error::File::Remote::SFTP, "SFTP authentication failed: #{$?}"
455
- end
456
- end
457
-
458
- def batch(files)
459
- results = []
460
- connection do |connection|
461
- files.each do |file|
462
- results.push(yield(file, connection))
463
- end
464
- end
465
- return results
466
- end
467
-
468
- def join_with_path(file)
469
- if @path
470
- [@path, file].join('/')
471
- else
472
- file
473
- end
474
- end
475
-
476
- end #SFTP
477
- end #Remote
478
-
479
- #Representation of the Project instance in the local
480
- #filesystem. Subclass of Filer::Dir; see Filer::Dir docs for
481
- #additional details.
482
- #
483
- #This is basically a local dir with various subdirs and files
484
- #containing the canonical representation of the project, including
485
- #data on remote resources, the project ID and subtitle, the audio files
486
- #themselves, and, when complete, an HTML transcript of that audio,
487
- #along with supporting CSS and Javascript files.
488
- class Local < Filer::Dir
489
- require 'fileutils'
490
- require 'securerandom'
491
-
492
- #Returns the dir path.
493
- attr_reader :path
494
-
495
- class << self
496
- #Constructor. Creates a directory in the filesystem for the
497
- #project.
498
- #
499
- # ==== Params
500
- # [name] Name of the associated project.
501
- # [base_dir] Path to the local directory into which the project
502
- # dir should be placed.
503
- # [template_dir] Path to the dir which will be used as a base
504
- # template for new projects.
505
- # ==== Returns
506
- # Project::Local instance.
507
- def create(name, base_dir, template_dir)
508
- local = super(File.join(base_dir, name))
509
- FileUtils.cp_r(File.join(template_dir, '.'), local)
510
- local.create_id
511
- local
512
- end
513
-
514
- #Takes the name of a project and a path. If there's a
515
- #directory with a matching name in the given path whose file
516
- #layout indicates it is a Project::Local instance (see 'ours?'
517
- #docs), returns a corresponding Project::Local instance.
518
- def named(string, path)
519
- match = super
520
- if match && ours?(match)
521
- return match
522
- end
523
- return
524
- end
525
-
526
- #Takes a Filer::Dir instance. Returns true or false depending on whether
527
- #the file layout inside the dir indicates it is a
528
- #Project::Local instance.
529
- def ours?(dir)
530
- File.exists?(dir.subdir('audio')) && File.exists?(dir.subdir('audio', 'originals'))
531
- end
532
-
533
- #Takes the name of a project and returns true if it is a valid
534
- #name for a directory in the local filesystem, false if not.
535
- def valid_name?(name)
536
- Utility.in_temp_dir do |dir|
537
- begin
538
- FileUtils.mkdir(File.join(dir, name))
539
- rescue Errno::ENOENT
540
- return false
541
- end #begin
542
- return File.exists?(File.join(dir, name))
543
- end #Utility.in_temp_dir do...
544
- end
545
-
546
- #Takes one or more symbols. Adds corresponding getter/setter
547
- #and delete method(s) to Project::Local, which read (getter)
548
- #and write (setter) and delete corresponding text files in the
549
- #data directory.
550
- #
551
- #So, for example, 'data_file_accessor :name' would allow you
552
- #to later create the file 'data/foo.txt' in the project dir by
553
- #calling 'project.local.name = "Foo"', read that same file via
554
- #'project.local.name', and delete the file via
555
- #'project.local.delete_name'
556
- def data_file_accessor(*syms)
557
- syms.each do |sym|
558
- define_method(sym) do
559
- file('data',"#{sym.to_s}.txt").read
560
- end
561
- define_method("#{sym.to_s}=".to_sym) do |value|
562
- file('data',"#{sym.to_s}.txt").write(value)
563
- end
564
- define_method("delete_#{sym.to_s}".to_sym) do
565
- if File.exists? file('data',"#{sym.to_s}.txt")
566
- File.delete(file('data',"#{sym.to_s}.txt"))
567
- end
568
- end
569
- end
570
- end
571
- end #class << self
572
-
573
- #Calling 'subtitle' will read 'data/subtitle.txt'; calling
574
- #'subtitle=' will write 'data/subtitle.txt'; calling
575
- #'delete_subtitle' will delete 'data/subtitle.txt'.
576
- data_file_accessor :subtitle
577
-
578
- #Returns the ID of the project, as stored in 'data/id.txt'.
579
- def id
580
- file('data','id.txt').read
581
- end
582
-
583
- #Creates a file storing the canonical ID of the project in
584
- #'data/id.txt'. Raises an exception if the file already exists.
585
- def create_id
586
- if id
587
- raise Error, "id already exists"
588
- end
589
- file('data','id.txt').write(SecureRandom.hex(16))
590
- end
591
- end #Local
592
198
  end #Project
593
199
  end #Typingpool
@@ -0,0 +1,17 @@
1
+ module Typingpool
2
+ class Template
3
+
4
+ #A Template::Assignment works just like a regular template, except
5
+ #that within each transcript dir (Config#transcript and the
6
+ #built-in app template dir) we search within a subdir called
7
+ #'assignment' first, then, after all the 'assignment' subdirs have
8
+ #been search, we look in the original template dirs.
9
+ class Assignment < Template
10
+ def self.look_in_from_config(*args)
11
+ look_in = super(*args)
12
+ look_in.unshift(look_in.reject{|dir| dir.empty? }.map{|dir| File.join(dir, 'assignment') })
13
+ look_in.flatten
14
+ end
15
+ end #Assignment
16
+ end #Template
17
+ end #Typingpool
@@ -0,0 +1,77 @@
1
+ module Typingpool
2
+ class Template
3
+
4
+ #This subclass provides two utility methods to all templates:
5
+ #read, for including the text of another template, and render, for
6
+ #rendering another template. Read takes a relative path,
7
+ #documented below. Render is passed the same hash as the parent
8
+ #template, merged with an optional override hash, as documented
9
+ #below.
10
+ #
11
+ #This subclass also makes it easier to use a hash as the top-level
12
+ #variable namespace when rendering ERB templates.
13
+ class Env
14
+
15
+ #Construtor. Takes a hash to be passed to the template and a
16
+ #template (ERB).
17
+ def initialize(hash, template)
18
+ @hash = hash
19
+ @template = template
20
+ end
21
+
22
+ #Method passed into each template. Takes a relative path and
23
+ #returns the text of the file at that path.
24
+ #
25
+ #The relative path is resolved as in look_in above, with the
26
+ #following difference: the current directory and each parent
27
+ #directory of the active template is searched first, up to the
28
+ #root transcript directory (either Config#template, the built-in
29
+ #app template dir, or any dir that has been manually added to
30
+ #look_in).
31
+ def read(path)
32
+ @template.class.new(path, localized_look_in).read.strip
33
+ end
34
+
35
+ #Method passed into each template. Takes a reltive path and
36
+ #returns the *rendered* text of the ERB template at that
37
+ #path. Can also take an optional hash, which will be merged into
38
+ #the parent template's hash and passed to the included
39
+ #template. If the optional hash it not passed, the parent
40
+ #template's hash will be passed to the included template
41
+ #unmodified.
42
+ #
43
+ #The relative path is resolved as described in the docs for
44
+ #Template::Env#read.
45
+ def render(path, hash={})
46
+ @template.class.new(path, localized_look_in).render(@hash.merge(hash)).strip
47
+ end
48
+
49
+ def get_binding
50
+ binding()
51
+ end
52
+
53
+ protected
54
+
55
+ def localized_look_in
56
+ look_in = []
57
+ path = @template.full_path
58
+ until @template.look_in.include? path = File.dirname(path)
59
+ look_in.push(path)
60
+ end
61
+ look_in.push(path, (@template.look_in - [path])).flatten
62
+ end
63
+
64
+ def method_missing(key, value=nil)
65
+ if value
66
+ key = key.to_s.sub(/=$/, '')
67
+ @hash[key.to_sym] = value
68
+ end
69
+ if @hash.has_key? key
70
+ @hash[key]
71
+ elsif @hash.has_key? key.to_s
72
+ @hash[key.to_s]
73
+ end
74
+ end
75
+ end #Env
76
+ end #Template
77
+ end #Typingpool
@@ -84,92 +84,7 @@ module Typingpool
84
84
  def extensions
85
85
  ['.html.erb', '.erb', '']
86
86
  end
87
-
88
-
89
- #A Template::Assignment works just like a regular template, except
90
- #that within each transcript dir (Config#transcript and the
91
- #built-in app template dir) we search within a subdir called
92
- #'assignment' first, then, after all the 'assignment' subdirs have
93
- #been search, we look in the original template dirs.
94
- class Assignment < Template
95
- def self.look_in_from_config(*args)
96
- look_in = super(*args)
97
- look_in.unshift(look_in.reject{|dir| dir.empty? }.map{|dir| File.join(dir, 'assignment') })
98
- look_in.flatten
99
- end
100
- end #Assignment
101
-
102
- #This subclass provides two utility methods to all templates:
103
- #read, for including the text of another template, and render, for
104
- #rendering another template. Read takes a relative path,
105
- #documented below. Render is passed the same hash as the parent
106
- #template, merged with an optional override hash, as documented
107
- #below.
108
- #
109
- #This subclass also makes it easier to use a hash as the top-level
110
- #variable namespace when rendering ERB templates.
111
- class Env
112
-
113
- #Construtor. Takes a hash to be passed to the template and a
114
- #template (ERB).
115
- def initialize(hash, template)
116
- @hash = hash
117
- @template = template
118
- end
119
-
120
- #Method passed into each template. Takes a relative path and
121
- #returns the text of the file at that path.
122
- #
123
- #The relative path is resolved as in look_in above, with the
124
- #following difference: the current directory and each parent
125
- #directory of the active template is searched first, up to the
126
- #root transcript directory (either Config#template, the built-in
127
- #app template dir, or any dir that has been manually added to
128
- #look_in).
129
- def read(path)
130
- @template.class.new(path, localized_look_in).read.strip
131
- end
132
-
133
- #Method passed into each template. Takes a reltive path and
134
- #returns the *rendered* text of the ERB template at that
135
- #path. Can also take an optional hash, which will be merged into
136
- #the parent template's hash and passed to the included
137
- #template. If the optional hash it not passed, the parent
138
- #template's hash will be passed to the included template
139
- #unmodified.
140
- #
141
- #The relative path is resolved as described in the docs for
142
- #Template::Env#read.
143
- def render(path, hash={})
144
- @template.class.new(path, localized_look_in).render(@hash.merge(hash)).strip
145
- end
146
-
147
- def get_binding
148
- binding()
149
- end
150
-
151
- protected
152
-
153
- def localized_look_in
154
- look_in = []
155
- path = @template.full_path
156
- until @template.look_in.include? path = File.dirname(path)
157
- look_in.push(path)
158
- end
159
- look_in.push(path, (@template.look_in - [path])).flatten
160
- end
161
-
162
- def method_missing(key, value=nil)
163
- if value
164
- key = key.to_s.sub(/=$/, '')
165
- @hash[key.to_sym] = value
166
- end
167
- if @hash.has_key? key
168
- @hash[key]
169
- elsif @hash.has_key? key.to_s
170
- @hash[key.to_s]
171
- end
172
- end
173
- end #Env
87
+ require 'typingpool/template/assignment'
88
+ require 'typingpool/template/env'
174
89
  end #Template
175
90
  end #Typingpool