typingpool 0.7.0 → 0.7.1

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 (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