thingfish 0.5.0.pre20160707192835

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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.simplecov +7 -0
  3. data/History.rdoc +5 -0
  4. data/LICENSE +29 -0
  5. data/Manifest.txt +39 -0
  6. data/Procfile +4 -0
  7. data/README.rdoc +92 -0
  8. data/Rakefile +92 -0
  9. data/bin/tfprocessord +6 -0
  10. data/bin/thingfish +10 -0
  11. data/etc/thingfish.conf.example +26 -0
  12. data/lib/strelka/app/metadata.rb +38 -0
  13. data/lib/strelka/httprequest/metadata.rb +70 -0
  14. data/lib/thingfish.rb +43 -0
  15. data/lib/thingfish/behaviors.rb +263 -0
  16. data/lib/thingfish/datastore.rb +55 -0
  17. data/lib/thingfish/datastore/memory.rb +93 -0
  18. data/lib/thingfish/handler.rb +728 -0
  19. data/lib/thingfish/metastore.rb +55 -0
  20. data/lib/thingfish/metastore/memory.rb +201 -0
  21. data/lib/thingfish/mixins.rb +57 -0
  22. data/lib/thingfish/processor.rb +79 -0
  23. data/lib/thingfish/processor/mp3.rb +167 -0
  24. data/lib/thingfish/processordaemon.rb +16 -0
  25. data/lib/thingfish/spechelpers.rb +165 -0
  26. data/spec/data/APIC-1-image.mp3 +0 -0
  27. data/spec/data/APIC-2-images.mp3 +0 -0
  28. data/spec/data/PIC-1-image.mp3 +0 -0
  29. data/spec/data/PIC-2-images.mp3 +0 -0
  30. data/spec/helpers.rb +67 -0
  31. data/spec/spec.opts +4 -0
  32. data/spec/thingfish/datastore/memory_spec.rb +19 -0
  33. data/spec/thingfish/datastore_spec.rb +64 -0
  34. data/spec/thingfish/handler_spec.rb +838 -0
  35. data/spec/thingfish/metastore/memory_spec.rb +17 -0
  36. data/spec/thingfish/metastore_spec.rb +96 -0
  37. data/spec/thingfish/mixins_spec.rb +63 -0
  38. data/spec/thingfish/processor/mp3_spec.rb +50 -0
  39. data/spec/thingfish/processor_spec.rb +65 -0
  40. data/spec/thingfish_spec.rb +23 -0
  41. metadata +244 -0
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../../helpers'
4
+
5
+ require 'securerandom'
6
+ require 'rspec'
7
+ require 'thingfish/metastore/memory'
8
+ require 'thingfish/behaviors'
9
+
10
+
11
+ describe Thingfish::Metastore::Memory do
12
+
13
+ it_behaves_like "a Thingfish metastore"
14
+
15
+ end
16
+
17
+ # vim: set nosta noet ts=4 sw=4 ft=rspec:
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../helpers'
4
+
5
+ require 'rspec'
6
+ require 'thingfish/metastore'
7
+
8
+ class TestingMetastore < Thingfish::Metastore
9
+ end
10
+
11
+
12
+ describe Thingfish::Metastore do
13
+
14
+ before( :all ) do
15
+ setup_logging()
16
+ end
17
+
18
+
19
+ it "is abstract" do
20
+ expect { described_class.new }.to raise_error( NoMethodError, /private/i )
21
+ end
22
+
23
+
24
+ it "acts as a factory for its concrete derivatives" do
25
+ expect( described_class.create('testing') ).to be_a( TestingMetastore )
26
+ end
27
+
28
+
29
+ describe "an instance of a concrete derivative" do
30
+
31
+ let( :store ) { described_class.create('testing') }
32
+
33
+ it "raises an error if it doesn't implement #oids" do
34
+ expect { store.oids }.to raise_error( NotImplementedError, /oids/ )
35
+ end
36
+
37
+ it "raises an error if it doesn't implement #each_oid" do
38
+ expect { store.each_oid }.to raise_error( NotImplementedError, /each_oid/ )
39
+ end
40
+
41
+ it "raises an error if it doesn't implement #fetch" do
42
+ expect { store.fetch(TEST_UUID) }.to raise_error( NotImplementedError, /fetch/ )
43
+ end
44
+
45
+ it "raises an error if it doesn't implement #fetch_value" do
46
+ expect { store.fetch_value(TEST_UUID, :format) }.
47
+ to raise_error( NotImplementedError, /fetch_value/ )
48
+ end
49
+
50
+ it "raises an error if it doesn't implement #search" do
51
+ expect { store.search(limit: 100) }.to raise_error( NotImplementedError, /search/ )
52
+ end
53
+
54
+ it "raises an error if it doesn't implement #save" do
55
+ expect {
56
+ store.save( TEST_UUID, {name: 'foo'} )
57
+ }.to raise_error( NotImplementedError, /save/ )
58
+ end
59
+
60
+ it "raises an error if it doesn't implement #merge" do
61
+ expect {
62
+ store.merge( TEST_UUID, {name: 'foo'} )
63
+ }.to raise_error( NotImplementedError, /merge/ )
64
+ end
65
+
66
+ it "raises an error if it doesn't implement #include?" do
67
+ expect { store.include?(TEST_UUID) }.to raise_error( NotImplementedError, /include\?/ )
68
+ end
69
+
70
+ it "raises an error if it doesn't implement #remove" do
71
+ expect { store.remove(TEST_UUID) }.to raise_error( NotImplementedError, /remove/ )
72
+ end
73
+
74
+ it "raises an error if it doesn't implement #remove_except" do
75
+ expect { store.remove_except(TEST_UUID, :format) }.
76
+ to raise_error( NotImplementedError, /remove_except/ )
77
+ end
78
+
79
+ it "raises an error if it doesn't implement #size" do
80
+ expect { store.size }.to raise_error( NotImplementedError, /size/ )
81
+ end
82
+
83
+ it "raises an error if it doesn't implement #fetch_related_oids" do
84
+ expect {
85
+ store.fetch_related_oids( TEST_UUID )
86
+ }.to raise_error( NotImplementedError, /fetch_related_oids/i )
87
+ end
88
+
89
+ it "provides a transactional block method" do
90
+ expect {|block| store.transaction(&block) }.to yield_with_no_args
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ # vim: set nosta noet ts=4 sw=4 ft=rspec:
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env rspec -cfd -b
2
+ # vim: set noet nosta sw=4 ts=4 :
3
+
4
+ require_relative '../helpers'
5
+
6
+ require 'rspec'
7
+
8
+ require 'thingfish/mixins'
9
+
10
+
11
+ describe Thingfish, 'mixins' do
12
+
13
+ # A collection of functions for dealing with object IDs.
14
+ describe 'Normalization' do
15
+
16
+ it 'can generate a new object ID' do
17
+ expect( Thingfish::Normalization.make_object_id ).to match( UUID_PATTERN )
18
+ end
19
+
20
+ it 'can normalize an object ID' do
21
+ expect(
22
+ Thingfish::Normalization.normalize_oid( TEST_UUID.upcase )
23
+ ).to_not match( /[A-Z]/ )
24
+ end
25
+
26
+ it 'can normalize Hash metadata keys' do
27
+ metadata = { :pork => 1, :sausaged => 2 }
28
+ expect( Thingfish::Normalization.normalize_keys(metadata) ).
29
+ to eq({ 'pork' => 1, 'sausaged' => 2 })
30
+ end
31
+
32
+ it 'can normalize an Array of metadata keys' do
33
+ values = [ :pork, :sausaged ]
34
+ expect( Thingfish::Normalization.normalize_keys(values) ).
35
+ to eq([ 'pork', 'sausaged' ])
36
+ expect( values.first ).to be( :pork )
37
+ end
38
+
39
+ it "won't modify the original array of metadata keys" do
40
+ values = [ :pork, :sausaged ]
41
+ normalized = Thingfish::Normalization.normalize_keys( values )
42
+
43
+ expect( values.first ).to be( :pork )
44
+ expect( normalized ).to_not be( values )
45
+ end
46
+
47
+ it "replaces non metadata key characters with underscores" do
48
+ expect( Thingfish::Normalization::normalize_key('Sausaged!') ).to eq( 'sausaged_' )
49
+ expect( Thingfish::Normalization::normalize_key('SO sausaged') ).to eq( 'so_sausaged' )
50
+ expect( Thingfish::Normalization::normalize_key('*/porky+-') ).to eq( '_porky_' )
51
+ end
52
+
53
+ it "preserves colons in metadata keys" do
54
+ expect( Thingfish::Normalization::normalize_key('pork:sausaged') ).
55
+ to eq( 'pork:sausaged' )
56
+ end
57
+
58
+
59
+ end # module Normalization
60
+
61
+
62
+ end
63
+
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../../helpers'
4
+
5
+ require 'rspec'
6
+ require 'thingfish/processor'
7
+
8
+ require 'strelka/httprequest/metadata'
9
+
10
+
11
+ describe Thingfish::Processor, "MP3" do
12
+
13
+ before( :all ) do
14
+ Strelka::HTTPRequest.class_eval { include Strelka::HTTPRequest::Metadata }
15
+ end
16
+
17
+
18
+ let( :processor ) { described_class.create(:mp3) }
19
+
20
+ let( :factory ) do
21
+ Mongrel2::RequestFactory.new(
22
+ :route => '/',
23
+ :headers => {:accept => '*/*'})
24
+ end
25
+
26
+
27
+ it "extracts metadata from uploaded MP3 ID3 tags" do
28
+ req = factory.post( '/tf', fixture_data('APIC-1-image.mp3'), 'Content-type' => 'audio/mp3' )
29
+
30
+ processor.process_request( req )
31
+
32
+ expect( req.metadata ).to include( 'mp3:artist', 'mp3:bitrate', 'mp3:comments' )
33
+ end
34
+
35
+
36
+ it "attaches album art as a related resource" do
37
+ req = factory.post( '/tf', fixture_data('APIC-1-image.mp3'), 'Content-type' => 'audio/mp3' )
38
+
39
+ processor.process_request( req )
40
+
41
+ related = req.related_resources
42
+ expect( related.size ).to eq( 1 )
43
+ expect( related.values.first ).
44
+ to include( 'format' => 'image/jpeg', 'extent' => 7369, 'relationship' => 'album-art' )
45
+ expect( related.keys.first ).to respond_to( :read )
46
+ end
47
+
48
+ end
49
+
50
+ # vim: set nosta noet ts=4 sw=4 ft=rspec:
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../helpers'
4
+
5
+ require 'rspec'
6
+ require 'thingfish/processor'
7
+
8
+
9
+ describe Thingfish::Processor do
10
+
11
+ before( :all ) do
12
+ setup_logging()
13
+ end
14
+
15
+
16
+ it "has pluggability" do
17
+ expect( described_class.plugin_type ).to eq( 'Processor' )
18
+ end
19
+
20
+
21
+ it "defines a (no-op) method for handling requests" do
22
+ expect {
23
+ described_class.new.on_request( nil )
24
+ }.to_not raise_error
25
+ end
26
+
27
+
28
+ it "defines a (no-op) method for handling responses" do
29
+ expect {
30
+ described_class.new.on_response( nil )
31
+ }.to_not raise_error
32
+ end
33
+
34
+
35
+ describe "a subclass" do
36
+
37
+ let!( :subclass ) { Class.new(described_class) }
38
+
39
+ it "can declare a list of media types it handles" do
40
+ subclass.handled_types( 'image/*', 'video/*' )
41
+ expect( subclass.handled_types.size ).to be( 2 )
42
+ expect( subclass.handled_types[0] ).to be_a( Strelka::HTTPRequest::MediaType )
43
+ end
44
+
45
+ describe "instance" do
46
+
47
+ let!( :instance ) do
48
+ subclass.handled_types( 'audio/mpeg', 'audio/mpg', 'audio/mp3' )
49
+ subclass.new
50
+ end
51
+
52
+
53
+ it "knows that it doesn't handle a type it hasn't registered" do
54
+ expect( instance ).to_not be_handled_type( 'image/png' )
55
+ expect( instance ).to be_handled_type( 'audio/mp3' )
56
+ end
57
+
58
+
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+
65
+ # vim: set nosta noet ts=4 sw=4 ft=rspec:
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative 'helpers'
4
+
5
+ require 'rspec'
6
+ require 'thingfish'
7
+
8
+
9
+ describe Thingfish do
10
+
11
+ it "returns a version string if asked" do
12
+ expect( described_class.version_string ).to match( /\w+ [\d.]+/ )
13
+ end
14
+
15
+
16
+ it "returns a version string with a build number if asked" do
17
+ expect( described_class.version_string(true) ).
18
+ to match(/\w+ [\d.]+ \(build [[:xdigit:]]+\)/)
19
+ end
20
+
21
+ end
22
+
23
+ # vim: set nosta noet ts=4 sw=4 ft=rspec:
metadata ADDED
@@ -0,0 +1,244 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thingfish
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0.pre20160707192835
5
+ platform: ruby
6
+ authors:
7
+ - Michael Granger
8
+ - Mahlon E. Smith
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain:
12
+ - |
13
+ -----BEGIN CERTIFICATE-----
14
+ MIIDMDCCAhigAwIBAgIBAjANBgkqhkiG9w0BAQUFADA+MQwwCgYDVQQDDANnZWQx
15
+ GTAXBgoJkiaJk/IsZAEZFglGYWVyaWVNVUQxEzARBgoJkiaJk/IsZAEZFgNvcmcw
16
+ HhcNMTYwNjAyMDE1NTQ2WhcNMTcwNjAyMDE1NTQ2WjA+MQwwCgYDVQQDDANnZWQx
17
+ GTAXBgoJkiaJk/IsZAEZFglGYWVyaWVNVUQxEzARBgoJkiaJk/IsZAEZFgNvcmcw
18
+ ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDb92mkyYwuGBg1oRxt2tkH
19
+ +Uo3LAsaL/APBfSLzy8o3+B3AUHKCjMUaVeBoZdWtMHB75X3VQlvXfZMyBxj59Vo
20
+ cDthr3zdao4HnyrzAIQf7BO5Y8KBwVD+yyXCD/N65TTwqsQnO3ie7U5/9ut1rnNr
21
+ OkOzAscMwkfQxBkXDzjvAWa6UF4c5c9kR/T79iA21kDx9+bUMentU59aCJtUcbxa
22
+ 7kcKJhPEYsk4OdxR9q2dphNMFDQsIdRO8rywX5FRHvcb+qnXC17RvxLHtOjysPtp
23
+ EWsYoZMxyCDJpUqbwoeiM+tAHoz2ABMv3Ahie3Qeb6+MZNAtMmaWfBx3dg2u+/WN
24
+ AgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBSZ0hCV
25
+ qoHr122fGKelqffzEQBhszANBgkqhkiG9w0BAQUFAAOCAQEAF2XCzjfTFxkcVvuj
26
+ hhBezFkZnMDYtWezg4QCkR0RHg4sl1LdXjpvvI59SIgD/evD1hOteGKsXqD8t0E4
27
+ OPAWWv/z+JRma72zeYsBZLSDRPIUvBoul6qCpvY0MiWTh496mFwOxT5lvSAUoh+U
28
+ pQ/MQeH/yC6hbGp7IYska6J8T4z5XkYqafYZ3eKQ8H+xPd/z+gYx+jd0PfkWf1Wk
29
+ QQdziL01SKBHf33OAH/p/puCpwS+ZDfgnNx5oMijWbc671UXkrt7zjD0kGakq+9I
30
+ hnfm736z8j1wvWddqf45++gwPpDr1E4zoAq2PgRl/WBNyR0hfoZLpi3TnHu3eC0x
31
+ uALKNA==
32
+ -----END CERTIFICATE-----
33
+ date: 2016-07-08 00:00:00.000000000 Z
34
+ dependencies:
35
+ - !ruby/object:Gem::Dependency
36
+ name: strelka
37
+ requirement: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.9'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '0.9'
49
+ - !ruby/object:Gem::Dependency
50
+ name: mongrel2
51
+ requirement: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '0.43'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '0.43'
63
+ - !ruby/object:Gem::Dependency
64
+ name: hoe-mercurial
65
+ requirement: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.4'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '1.4'
77
+ - !ruby/object:Gem::Dependency
78
+ name: hoe-deveiate
79
+ requirement: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.8'
84
+ type: :development
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '0.8'
91
+ - !ruby/object:Gem::Dependency
92
+ name: hoe-highline
93
+ requirement: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '0.2'
98
+ type: :development
99
+ prerelease: false
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '0.2'
105
+ - !ruby/object:Gem::Dependency
106
+ name: rdoc
107
+ requirement: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '4.0'
112
+ type: :development
113
+ prerelease: false
114
+ version_requirements: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '4.0'
119
+ - !ruby/object:Gem::Dependency
120
+ name: simplecov
121
+ requirement: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '0.7'
126
+ type: :development
127
+ prerelease: false
128
+ version_requirements: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '0.7'
133
+ - !ruby/object:Gem::Dependency
134
+ name: ruby-mp3info
135
+ requirement: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '0.8'
140
+ type: :development
141
+ prerelease: false
142
+ version_requirements: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: '0.8'
147
+ - !ruby/object:Gem::Dependency
148
+ name: hoe
149
+ requirement: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: '3.15'
154
+ type: :development
155
+ prerelease: false
156
+ version_requirements: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '3.15'
161
+ description: |-
162
+ Thingfish is a extensible, web-based digital asset manager. It can be used to
163
+ store chunks of data on the network in an application-independent way, link the
164
+ chunks together with metadata, and then search for the chunk you need later and
165
+ fetch it, all through a REST API.
166
+ email:
167
+ - ged@FaerieMUD.org
168
+ - mahlon@martini.nu
169
+ executables:
170
+ - tfprocessord
171
+ - thingfish
172
+ extensions: []
173
+ extra_rdoc_files:
174
+ - History.rdoc
175
+ - Manifest.txt
176
+ - README.rdoc
177
+ files:
178
+ - ".simplecov"
179
+ - History.rdoc
180
+ - LICENSE
181
+ - Manifest.txt
182
+ - Procfile
183
+ - README.rdoc
184
+ - Rakefile
185
+ - bin/tfprocessord
186
+ - bin/thingfish
187
+ - etc/thingfish.conf.example
188
+ - lib/strelka/app/metadata.rb
189
+ - lib/strelka/httprequest/metadata.rb
190
+ - lib/thingfish.rb
191
+ - lib/thingfish/behaviors.rb
192
+ - lib/thingfish/datastore.rb
193
+ - lib/thingfish/datastore/memory.rb
194
+ - lib/thingfish/handler.rb
195
+ - lib/thingfish/metastore.rb
196
+ - lib/thingfish/metastore/memory.rb
197
+ - lib/thingfish/mixins.rb
198
+ - lib/thingfish/processor.rb
199
+ - lib/thingfish/processor/mp3.rb
200
+ - lib/thingfish/processordaemon.rb
201
+ - lib/thingfish/spechelpers.rb
202
+ - spec/data/APIC-1-image.mp3
203
+ - spec/data/APIC-2-images.mp3
204
+ - spec/data/PIC-1-image.mp3
205
+ - spec/data/PIC-2-images.mp3
206
+ - spec/helpers.rb
207
+ - spec/spec.opts
208
+ - spec/thingfish/datastore/memory_spec.rb
209
+ - spec/thingfish/datastore_spec.rb
210
+ - spec/thingfish/handler_spec.rb
211
+ - spec/thingfish/metastore/memory_spec.rb
212
+ - spec/thingfish/metastore_spec.rb
213
+ - spec/thingfish/mixins_spec.rb
214
+ - spec/thingfish/processor/mp3_spec.rb
215
+ - spec/thingfish/processor_spec.rb
216
+ - spec/thingfish_spec.rb
217
+ homepage: http://bitbucket.org/ged/thingfish
218
+ licenses:
219
+ - BSD
220
+ - BSD
221
+ metadata: {}
222
+ post_install_message:
223
+ rdoc_options:
224
+ - "--main"
225
+ - README.rdoc
226
+ require_paths:
227
+ - lib
228
+ required_ruby_version: !ruby/object:Gem::Requirement
229
+ requirements:
230
+ - - ">="
231
+ - !ruby/object:Gem::Version
232
+ version: 2.0.0
233
+ required_rubygems_version: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - ">"
236
+ - !ruby/object:Gem::Version
237
+ version: 1.3.1
238
+ requirements: []
239
+ rubyforge_project:
240
+ rubygems_version: 2.4.8
241
+ signing_key:
242
+ specification_version: 4
243
+ summary: Thingfish is a extensible, web-based digital asset manager
244
+ test_files: []